diff --git a/.drone.yml b/.drone.yml index 779952f..5696614 100644 --- a/.drone.yml +++ b/.drone.yml @@ -9,7 +9,7 @@ platform: steps: - name: frontend - image: node:24-alpine + image: node:22-alpine commands: - cd web - npm install --network-timeout=100000 diff --git a/Dockerfile b/Dockerfile index 4568784..6e099f6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # Multi-stage Dockerfile for happyDeliver with integrated MTA # Stage 1: Build the Svelte application -FROM node:24-alpine AS nodebuild +FROM node:22-alpine AS nodebuild WORKDIR /build @@ -34,7 +34,7 @@ RUN go generate ./... && \ # Stage 3: Prepare perl and spamass-milt FROM alpine:3 AS pl -RUN echo "@edge https://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories && \ +RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories && \ apk add --no-cache \ build-base \ libmilter-dev \ @@ -55,7 +55,7 @@ RUN echo "@edge https://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/ap perl-json-xs \ perl-list-moreutils \ perl-moose \ - perl-net-idn-encode@edge \ + perl-net-idn-encode@testing \ perl-net-ssleay \ perl-netaddr-ip \ perl-package-stash \ @@ -86,7 +86,7 @@ RUN wget https://download.savannah.nongnu.org/releases/spamass-milt/spamass-milt FROM alpine:3 # Install all required packages -RUN echo "@edge https://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories && \ +RUN echo "@testing https://dl-cdn.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories && \ apk add --no-cache \ bash \ ca-certificates \ @@ -106,7 +106,7 @@ RUN echo "@edge https://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/ap perl-json-xs \ perl-list-moreutils \ perl-moose \ - perl-net-idn-encode@edge \ + perl-net-idn-encode@testing \ perl-net-ssleay \ perl-netaddr-ip \ perl-package-stash \ @@ -121,7 +121,6 @@ RUN echo "@edge https://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/ap perl-xml-libxml \ postfix \ postfix-pcre \ - rspamd \ spamassassin \ spamassassin-client \ supervisor \ @@ -144,11 +143,8 @@ RUN mkdir -p /etc/happydeliver \ /var/lib/authentication_milter \ /var/spool/postfix/authentication_milter \ /var/spool/postfix/spamassassin \ - /var/spool/postfix/rspamd \ && chown -R happydeliver:happydeliver /var/lib/happydeliver /var/log/happydeliver \ - && chown -R mail:mail /var/spool/postfix/authentication_milter /var/spool/postfix/spamassassin \ - && chown rspamd:mail /var/spool/postfix/rspamd \ - && chmod 750 /var/spool/postfix/rspamd + && chown -R mail:mail /var/spool/postfix/authentication_milter /var/spool/postfix/spamassassin # Copy the built application COPY --from=builder /build/happyDeliver /usr/local/bin/happyDeliver @@ -158,7 +154,6 @@ RUN chmod +x /usr/local/bin/happyDeliver COPY docker/postfix/ /etc/postfix/ COPY docker/authentication_milter/authentication_milter.json /etc/authentication_milter.json COPY docker/spamassassin/ /etc/mail/spamassassin/ -COPY docker/rspamd/local.d/ /etc/rspamd/local.d/ COPY docker/supervisor/ /etc/supervisor/ COPY docker/entrypoint.sh /entrypoint.sh @@ -170,21 +165,11 @@ RUN chmod +x /entrypoint.sh EXPOSE 25 8080 # Default configuration -ENV HAPPYDELIVER_DATABASE_TYPE=sqlite \ - HAPPYDELIVER_DATABASE_DSN=/var/lib/happydeliver/happydeliver.db \ - HAPPYDELIVER_DOMAIN=happydeliver.local \ - HAPPYDELIVER_ADDRESS_PREFIX=test- \ - HAPPYDELIVER_DNS_TIMEOUT=5s \ - HAPPYDELIVER_HTTP_TIMEOUT=10s \ - HAPPYDELIVER_RSPAMD_API_URL=http://127.0.0.1:11334 +ENV HAPPYDELIVER_DATABASE_TYPE=sqlite HAPPYDELIVER_DATABASE_DSN=/var/lib/happydeliver/happydeliver.db HAPPYDELIVER_DOMAIN=happydeliver.local HAPPYDELIVER_ADDRESS_PREFIX=test- HAPPYDELIVER_DNS_TIMEOUT=5s HAPPYDELIVER_HTTP_TIMEOUT=10s HAPPYDELIVER_RBL=zen.spamhaus.org,bl.spamcop.net,b.barracudacentral.org,dnsbl.sorbs.net,dnsbl-1.uceprotect.net,bl.mailspike.net # Volume for persistent data VOLUME ["/var/lib/happydeliver", "/var/log/happydeliver"] -# Health check -HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \ - CMD wget --quiet --tries=1 --spider http://localhost:8080/api/status || exit 1 - # Set entrypoint ENTRYPOINT ["/entrypoint.sh"] CMD ["supervisord", "-c", "/etc/supervisor/supervisord.conf"] diff --git a/README.md b/README.md index 4c4013b..a8f79e3 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ An open-source email deliverability testing platform that analyzes test emails a ## Features -- **Complete Email Analysis**: Analyzes SPF, DKIM, DMARC, BIMI, ARC, SpamAssassin and rspamd scores, DNS records, blacklist status, content quality, and more +- **Complete Email Analysis**: Analyzes SPF, DKIM, DMARC, BIMI, ARC, SpamAssassin scores, DNS records, blacklist status, content quality, and more - **REST API**: Full-featured API for creating tests and retrieving reports - **LMTP Server**: Built-in LMTP server for seamless MTA integration - **Scoring System**: Gives A to F grades and scoring with weighted factors across dns, authentication, spam, blacklists, content, and headers @@ -26,7 +26,6 @@ The easiest way to run happyDeliver is using the all-in-one Docker container tha - **Postfix MTA**: Receives emails on port 25 - **authentication_milter**: Entreprise grade email authentication - **SpamAssassin**: Spam scoring and analysis -- **rspamd**: Second spam filter for cross-validated scoring - **happyDeliver API**: REST API server on port 8080 - **SQLite Database**: Persistent storage for tests and reports @@ -38,7 +37,7 @@ git clone https://git.nemunai.re/happyDomain/happyDeliver.git cd happydeliver # Edit docker-compose.yml to set your domain -# Change HAPPYDELIVER_DOMAIN environment variable and hostname +# Change HAPPYDELIVER_DOMAIN and HOSTNAME environment variables # Build and start docker-compose up -d @@ -64,54 +63,13 @@ docker run -d \ -p 25:25 \ -p 8080:8080 \ -e HAPPYDELIVER_DOMAIN=yourdomain.com \ - --hostname mail.yourdomain.com \ + -e HOSTNAME=mail.yourdomain.com \ -v $(pwd)/data:/var/lib/happydeliver \ -v $(pwd)/logs:/var/log/happydeliver \ happydeliver:latest ``` -#### 3. Configure TLS Certificates (Optional but Recommended) - -To enable TLS encryption for incoming SMTP connections, you can configure Postfix to use your SSL/TLS certificates. This is highly recommended for production deployments. - -##### Using docker-compose - -Add the certificate paths to your `docker-compose.yml`: - -```yaml -environment: - - POSTFIX_CERT_FILE=/etc/ssl/certs/mail.yourdomain.com.crt - - POSTFIX_KEY_FILE=/etc/ssl/private/mail.yourdomain.com.key -volumes: - - /path/to/your/certificate.crt:/etc/ssl/certs/mail.yourdomain.com.crt:ro - - /path/to/your/private.key:/etc/ssl/private/mail.yourdomain.com.key:ro -``` - -##### Using docker run - -```bash -docker run -d \ - --name happydeliver \ - -p 25:25 \ - -p 8080:8080 \ - -e HAPPYDELIVER_DOMAIN=yourdomain.com \ - -e POSTFIX_CERT_FILE=/etc/ssl/certs/mail.yourdomain.com.crt \ - -e POSTFIX_KEY_FILE=/etc/ssl/private/mail.yourdomain.com.key \ - --hostname mail.yourdomain.com \ - -v /path/to/your/certificate.crt:/etc/ssl/certs/mail.yourdomain.com.crt:ro \ - -v /path/to/your/private.key:/etc/ssl/private/mail.yourdomain.com.key:ro \ - -v $(pwd)/data:/var/lib/happydeliver \ - -v $(pwd)/logs:/var/log/happydeliver \ - happydeliver:latest -``` - -**Notes:** -- The certificate file should contain the full certificate chain (certificate + intermediate CAs) -- The private key file must be readable by the postfix user inside the container -- TLS is configured with `smtpd_tls_security_level = may`, which means it's opportunistic (STARTTLS supported but not required) -- If both environment variables are not set, Postfix will run without TLS support - -#### 4. Configure Network and DNS +#### 3. Configure Network and DNS ##### Open SMTP Port @@ -163,27 +121,10 @@ The server will start on `http://localhost:8080` by default. #### 3. Integrate with your existing e-mail setup -It is expected your setup annotate the email with eg. opendkim, spamassassin, rspamd, ... +It is expected your setup annotate the email with eg. opendkim, spamassassin, ... happyDeliver will not perform thoses checks, it relies instead on standard software to have real world annotations. -#### Receiver Hostname - -happyDeliver filters `Authentication-Results` headers by hostname to only trust headers added by your MTA (and not headers that may have been injected by the sender). By default, it uses the system hostname (`os.Hostname()`). - -If your MTA's `authserv-id` (the hostname at the beginning of `Authentication-Results` headers) differs from the machine running happyDeliver, you must set it explicitly: - -```bash -./happyDeliver server -receiver-hostname mail.example.com -``` - -Or via environment variable: -```bash -HAPPYDELIVER_RECEIVER_HOSTNAME=mail.example.com ./happyDeliver server -``` - -**How to find the correct value:** look at the `Authentication-Results` headers in a received email. They start with the authserv-id, e.g. `Authentication-Results: mail.example.com; spf=pass ...` — in this case, use `mail.example.com`. - -If the value is misconfigured, happyDeliver will log a warning when the last `Received` hop doesn't match the expected hostname. +Choose one of the following way to integrate happyDeliver in your existing setup: #### Postfix LMTP Transport @@ -287,7 +228,7 @@ The deliverability score is calculated from A to F based on: - **Authentication**: IPRev, SPF, DKIM, DMARC, BIMI and ARC validation - **Blacklist**: RBL/DNSBL checks - **Headers**: Required headers, MIME structure, Domain alignment -- **Spam**: SpamAssassin and rspamd scores (combined 50/50) +- **Spam**: SpamAssassin score - **Content**: HTML quality, links, images, unsubscribe ## Funding diff --git a/api/openapi.yaml b/api/openapi.yaml index 225e26c..7d2ec2c 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -169,72 +169,6 @@ paths: schema: $ref: '#/components/schemas/Error' - /domain: - post: - tags: - - tests - summary: Test a domain's email configuration - description: Analyzes DNS records (MX, SPF, DMARC, BIMI) for a domain without requiring an actual email to be sent. Returns results immediately. - operationId: testDomain - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/DomainTestRequest' - responses: - '200': - description: Domain test completed successfully - content: - application/json: - schema: - $ref: '#/components/schemas/DomainTestResponse' - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - - /blacklist: - post: - tags: - - tests - summary: Check an IP address against DNS blacklists - description: Tests a single IP address (IPv4 or IPv6) against configured DNS-based blacklists (RBLs) without requiring an actual email to be sent. Returns results immediately. - operationId: checkBlacklist - requestBody: - required: true - content: - application/json: - schema: - $ref: '#/components/schemas/BlacklistCheckRequest' - responses: - '200': - description: Blacklist check completed successfully - content: - application/json: - schema: - $ref: '#/components/schemas/BlacklistCheckResponse' - '400': - description: Invalid request - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - '500': - description: Internal server error - content: - application/json: - schema: - $ref: '#/components/schemas/Error' - /status: get: tags: @@ -333,8 +267,6 @@ components: $ref: '#/components/schemas/AuthenticationResults' spamassassin: $ref: '#/components/schemas/SpamAssassinResult' - rspamd: - $ref: '#/components/schemas/RspamdResult' dns_results: $ref: '#/components/schemas/DNSResults' blacklists: @@ -350,19 +282,6 @@ components: listed: false - rbl: "bl.spamcop.net" listed: false - whitelists: - type: object - additionalProperties: - type: array - items: - $ref: '#/components/schemas/BlacklistCheck' - description: Map of IP addresses to their DNS whitelist check results (informational only) - example: - "192.0.2.1": - - rbl: "list.dnswl.org" - listed: false - - rbl: "swl.spamhaus.org" - listed: false content_analysis: $ref: '#/components/schemas/ContentAnalysis' header_analysis: @@ -416,7 +335,7 @@ components: type: integer minimum: 0 maximum: 100 - description: Spam filter score (SpamAssassin + rspamd combined, in percentage) + description: SpamAssassin score (in percentage) example: 15 spam_grade: type: string @@ -679,21 +598,6 @@ components: description: Reverse DNS (PTR record) for the IP address example: "mail.example.com" - DKIMDomainInfo: - type: object - required: - - domain - - org_domain - properties: - domain: - type: string - description: DKIM signature domain - example: "mail.example.com" - org_domain: - type: string - description: Organizational domain extracted from DKIM domain (using Public Suffix List) - example: "example.com" - DomainAlignment: type: object properties: @@ -716,8 +620,9 @@ components: dkim_domains: type: array items: - $ref: '#/components/schemas/DKIMDomainInfo' - description: Domains from DKIM signatures with their organizational domains + type: string + description: Domains from DKIM signatures + example: ["example.com"] aligned: type: boolean description: Whether all domains align (strict alignment - exact match) @@ -789,7 +694,7 @@ components: properties: result: type: string - enum: [pass, fail, invalid, missing, none, neutral, softfail, temperror, permerror, declined, domain_pass, orgdomain_pass, skipped] + enum: [pass, fail, invalid, missing, none, neutral, softfail, temperror, permerror, declined] description: Authentication result example: "pass" domain: @@ -858,17 +763,6 @@ components: - is_spam - test_details properties: - deliverability_score: - type: integer - minimum: 0 - maximum: 100 - description: SpamAssassin deliverability score (0-100, higher is better) - example: 80 - deliverability_grade: - type: string - enum: [A+, A, B, C, D, E, F] - description: Letter grade for SpamAssassin deliverability score - example: "B" version: type: string description: SpamAssassin version @@ -926,71 +820,11 @@ components: format: float description: Score contribution of this test example: -1.9 - params: - type: string - description: Symbol parameters or options - example: "0.02" description: type: string description: Human-readable description of what this test checks example: "Bayes spam probability is 0 to 1%" - RspamdResult: - type: object - required: - - score - - threshold - - is_spam - - symbols - properties: - deliverability_score: - type: integer - minimum: 0 - maximum: 100 - description: rspamd deliverability score (0-100, higher is better) - example: 85 - deliverability_grade: - type: string - enum: [A+, A, B, C, D, E, F] - description: Letter grade for rspamd deliverability score - example: "A" - score: - type: number - format: float - description: rspamd spam score - example: -3.91 - threshold: - type: number - format: float - description: Score threshold for spam classification - example: 15.0 - action: - type: string - description: rspamd action (no action, add header, rewrite subject, soft reject, reject) - example: "no action" - is_spam: - type: boolean - description: Whether message is classified as spam (action is reject or soft reject) - example: false - server: - type: string - description: rspamd server that processed the message - example: "rspamd.example.com" - symbols: - type: object - additionalProperties: - $ref: '#/components/schemas/SpamTestDetail' - description: Map of triggered rspamd symbols to their details - example: - BAYES_HAM: - name: "BAYES_HAM" - score: -1.9 - params: "0.02" - report: - type: string - description: Full rspamd report (raw X-Spamd-Result header) - - DNSResults: type: object required: @@ -1278,90 +1112,3 @@ components: details: type: string description: Additional error details - - DomainTestRequest: - type: object - required: - - domain - properties: - domain: - type: string - pattern: '^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$' - description: Domain name to test (e.g., example.com) - example: "example.com" - - DomainTestResponse: - type: object - required: - - domain - - score - - grade - - dns_results - properties: - domain: - type: string - description: The tested domain name - example: "example.com" - score: - type: integer - minimum: 0 - maximum: 100 - description: Overall domain configuration score (0-100) - example: 85 - grade: - type: string - enum: [A+, A, B, C, D, E, F] - description: Letter grade representation of the score - example: "A" - dns_results: - $ref: '#/components/schemas/DNSResults' - - BlacklistCheckRequest: - type: object - required: - - ip - properties: - ip: - type: string - description: IPv4 or IPv6 address to check against blacklists - example: "192.0.2.1" - pattern: '^([0-9]{1,3}\.){3}[0-9]{1,3}$|^([0-9a-fA-F]{0,4}:){7}[0-9a-fA-F]{0,4}$|^::([0-9a-fA-F]{0,4}:){0,6}[0-9a-fA-F]{0,4}$|^([0-9a-fA-F]{0,4}:){1,6}:([0-9a-fA-F]{0,4}:){0,5}[0-9a-fA-F]{0,4}$' - - BlacklistCheckResponse: - type: object - required: - - ip - - blacklists - - listed_count - - score - - grade - properties: - ip: - type: string - description: The IP address that was checked - example: "192.0.2.1" - blacklists: - type: array - items: - $ref: '#/components/schemas/BlacklistCheck' - description: List of blacklist check results - listed_count: - type: integer - description: Number of blacklists that have this IP listed - example: 0 - score: - type: integer - minimum: 0 - maximum: 100 - description: Blacklist score (0-100, higher is better) - example: 100 - grade: - type: string - enum: [A+, A, B, C, D, E, F] - description: Letter grade representation of the score - example: "A+" - whitelists: - type: array - items: - $ref: '#/components/schemas/BlacklistCheck' - description: List of DNS whitelist check results (informational only) diff --git a/cmd/happyDeliver/main.go b/cmd/happyDeliver/main.go index 3caf4d1..af1d30f 100644 --- a/cmd/happyDeliver/main.go +++ b/cmd/happyDeliver/main.go @@ -33,8 +33,8 @@ import ( ) func main() { - fmt.Fprintln(os.Stderr, "happyDeliver - Email Deliverability Testing Platform") - fmt.Fprintf(os.Stderr, "Version: %s\n", version.Version) + fmt.Println("happyDeliver - Email Deliverability Testing Platform") + fmt.Printf("Version: %s\n", version.Version) cfg, err := config.ConsolidateConfig() if err != nil { @@ -52,18 +52,6 @@ func main() { if err := app.RunAnalyzer(cfg, flag.Args()[1:], os.Stdin, os.Stdout); err != nil { log.Fatalf("Analyzer error: %v", err) } - case "backup": - if err := app.RunBackup(cfg); err != nil { - log.Fatalf("Backup error: %v", err) - } - case "restore": - inputFile := "" - if len(flag.Args()) >= 2 { - inputFile = flag.Args()[1] - } - if err := app.RunRestore(cfg, inputFile); err != nil { - log.Fatalf("Restore error: %v", err) - } case "version": fmt.Println(version.Version) default: @@ -75,11 +63,9 @@ func main() { func printUsage() { fmt.Println("\nCommand availables:") - fmt.Println(" happyDeliver server - Start the API server") - fmt.Println(" happyDeliver analyze [-json] - Analyze email from stdin and output results to terminal") - fmt.Println(" happyDeliver backup - Backup database to stdout as JSON") - fmt.Println(" happyDeliver restore [file] - Restore database from JSON file or stdin") - fmt.Println(" happyDeliver version - Print version information") + fmt.Println(" happyDeliver server - Start the API server") + fmt.Println(" happyDeliver analyze [-json] - Analyze email from stdin and output results to terminal") + fmt.Println(" happyDeliver version - Print version information") fmt.Println("") flag.Usage() } diff --git a/docker-compose.yml b/docker-compose.yml index ccfd313..87521ef 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,14 +3,14 @@ services: build: context: . dockerfile: Dockerfile - image: happydomain/happydeliver:latest + image: happydeliver:latest container_name: happydeliver - # Set a hostname hostname: mail.happydeliver.local environment: - # Set your domain - HAPPYDELIVER_DOMAIN: happydeliver.local + # Set your domain and hostname + DOMAIN: happydeliver.local + HOSTNAME: mail.happydeliver.local ports: # SMTP port @@ -26,6 +26,13 @@ services: restart: unless-stopped + healthcheck: + test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8080/api/status"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 40s + volumes: data: logs: diff --git a/docker/README.md b/docker/README.md index 2199eeb..45cce6b 100644 --- a/docker/README.md +++ b/docker/README.md @@ -109,37 +109,12 @@ Default configuration for the Docker environment: The container accepts these environment variables: -- `HAPPYDELIVER_DOMAIN`: Email domain for test addresses (default: happydeliver.local) -- `HAPPYDELIVER_RECEIVER_HOSTNAME`: Hostname used to filter `Authentication-Results` headers (see below) -- `POSTFIX_CERT_FILE` / `POSTFIX_KEY_FILE`: TLS certificate and key paths for Postfix SMTP - -### Receiver Hostname - -happyDeliver filters `Authentication-Results` headers by hostname to only trust results from the expected MTA. By default, it uses the system hostname (i.e., the container's `--hostname`). - -In the all-in-one Docker container, the container hostname is also used as the `authserv-id` in the embedded Postfix and authentication_milter, so everything matches automatically. - -**When bypassing the embedded Postfix** (e.g., routing emails from your own MTA via LMTP), your MTA's `authserv-id` will likely differ from the container hostname. In that case, set `HAPPYDELIVER_RECEIVER_HOSTNAME` to your MTA's hostname: +- `DOMAIN`: Email domain for test addresses (default: happydeliver.local) +- `HOSTNAME`: Container hostname (default: mail.happydeliver.local) +Example: ```bash -docker run -d \ - -e HAPPYDELIVER_DOMAIN=example.com \ - -e HAPPYDELIVER_RECEIVER_HOSTNAME=mail.example.com \ - ... -``` - -To find the correct value, look at the `Authentication-Results` headers in a received email — they start with the authserv-id, e.g. `Authentication-Results: mail.example.com; spf=pass ...`. - -If the value is misconfigured, happyDeliver will log a warning when the last `Received` hop doesn't match the expected hostname. - -Example (all-in-one, no override needed): -```bash -docker run -e HAPPYDELIVER_DOMAIN=example.com --hostname mail.example.com ... -``` - -Example (external MTA integration): -```bash -docker run -e HAPPYDELIVER_DOMAIN=example.com -e HAPPYDELIVER_RECEIVER_HOSTNAME=mail.example.com ... +docker run -e DOMAIN=example.com -e HOSTNAME=mail.example.com ... ``` ## Volumes diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index ef45b61..99744f6 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -4,7 +4,7 @@ set -e echo "Starting happyDeliver container..." # Get environment variables with defaults -[ -n "${HOSTNAME}" ] || HOSTNAME=$(hostname) +HOSTNAME="${HOSTNAME:-mail.happydeliver.local}" HAPPYDELIVER_DOMAIN="${HAPPYDELIVER_DOMAIN:-happydeliver.local}" echo "Hostname: $HOSTNAME" @@ -15,10 +15,6 @@ mkdir -p /var/spool/postfix/authentication_milter chown mail:mail /var/spool/postfix/authentication_milter chmod 750 /var/spool/postfix/authentication_milter -mkdir -p /var/spool/postfix/rspamd -chown rspamd:mail /var/spool/postfix/rspamd -chmod 750 /var/spool/postfix/rspamd - # Create log directory mkdir -p /var/log/happydeliver /var/cache/authentication_milter /var/spool/authentication_milter /var/lib/authentication_milter /run/authentication_milter chown happydeliver:happydeliver /var/log/happydeliver @@ -29,15 +25,6 @@ echo "Configuring Postfix..." sed -i "s/__HOSTNAME__/${HOSTNAME}/g" /etc/postfix/main.cf sed -i "s/__DOMAIN__/${HAPPYDELIVER_DOMAIN}/g" /etc/postfix/main.cf -# Add certificates to postfix -[ -n "${POSTFIX_CERT_FILE}" ] && [ -n "${POSTFIX_KEY_FILE}" ] && { - cat <> /etc/postfix/main.cf -smtpd_tls_cert_file = ${POSTFIX_CERT_FILE} -smtpd_tls_key_file = ${POSTFIX_KEY_FILE} -smtpd_tls_security_level = may -EOF -} - # Replace placeholders in configurations sed -i "s/__HOSTNAME__/${HOSTNAME}/g" /etc/authentication_milter.json diff --git a/docker/postfix/main.cf b/docker/postfix/main.cf index 5a73fb3..fcdb75c 100644 --- a/docker/postfix/main.cf +++ b/docker/postfix/main.cf @@ -28,7 +28,7 @@ transport_maps = pcre:/etc/postfix/transport_maps # OpenDKIM for DKIM verification milter_default_action = accept milter_protocol = 6 -smtpd_milters = unix:/var/spool/postfix/authentication_milter/authentication_milter.sock unix:/var/spool/postfix/spamassassin/spamass-milter.sock unix:/var/spool/postfix/rspamd/rspamd-milter.sock +smtpd_milters = unix:/var/spool/postfix/authentication_milter/authentication_milter.sock unix:/var/spool/postfix/spamassassin/spamass-milter.sock non_smtpd_milters = $smtpd_milters # SPF policy checking diff --git a/docker/rspamd/local.d/actions.conf b/docker/rspamd/local.d/actions.conf deleted file mode 100644 index f3ed60c..0000000 --- a/docker/rspamd/local.d/actions.conf +++ /dev/null @@ -1,5 +0,0 @@ -no_action = 0; -reject = null; -add_header = null; -rewrite_subject = null; -greylist = null; \ No newline at end of file diff --git a/docker/rspamd/local.d/milter_headers.conf b/docker/rspamd/local.d/milter_headers.conf deleted file mode 100644 index 378b8a3..0000000 --- a/docker/rspamd/local.d/milter_headers.conf +++ /dev/null @@ -1,5 +0,0 @@ -# Add "extended Rspamd headers" -extended_spam_headers = true; - -skip_local = false; -skip_authenticated = false; \ No newline at end of file diff --git a/docker/rspamd/local.d/options.inc b/docker/rspamd/local.d/options.inc deleted file mode 100644 index 485d0c9..0000000 --- a/docker/rspamd/local.d/options.inc +++ /dev/null @@ -1,3 +0,0 @@ -# rspamd options for happyDeliver -# Disable Bayes learning to keep the setup stateless -use_redis = false; diff --git a/docker/rspamd/local.d/worker-proxy.inc b/docker/rspamd/local.d/worker-proxy.inc deleted file mode 100644 index 04c9a1d..0000000 --- a/docker/rspamd/local.d/worker-proxy.inc +++ /dev/null @@ -1,6 +0,0 @@ -# Enable rspamd milter proxy worker via Unix socket for Postfix integration -bind_socket = "/var/spool/postfix/rspamd/rspamd-milter.sock mode=0660 owner=rspamd group=mail"; -upstream "local" { - default = yes; - self_scan = yes; -} diff --git a/docker/spamassassin/local.cf b/docker/spamassassin/local.cf index ce9a31c..c248ef6 100644 --- a/docker/spamassassin/local.cf +++ b/docker/spamassassin/local.cf @@ -48,14 +48,3 @@ rbl_timeout 5 # Don't use user-specific rules user_scores_dsn_timeout 3 user_scores_sql_override 0 - -# Disable Validity network rules -dns_query_restriction deny sa-trusted.bondedsender.org -dns_query_restriction deny sa-accredit.habeas.com -dns_query_restriction deny bl.score.senderscore.com -score RCVD_IN_VALIDITY_CERTIFIED_BLOCKED 0 -score RCVD_IN_VALIDITY_RPBL_BLOCKED 0 -score RCVD_IN_VALIDITY_SAFE_BLOCKED 0 -score RCVD_IN_VALIDITY_CERTIFIED 0 -score RCVD_IN_VALIDITY_RPBL 0 -score RCVD_IN_VALIDITY_SAFE 0 \ No newline at end of file diff --git a/docker/supervisor/supervisord.conf b/docker/supervisor/supervisord.conf index 74f1810..c0c7002 100644 --- a/docker/supervisor/supervisord.conf +++ b/docker/supervisor/supervisord.conf @@ -33,16 +33,6 @@ stderr_logfile=/var/log/happydeliver/authentication_milter.log user=mail group=mail -# rspamd spam filter -[program:rspamd] -command=/usr/bin/rspamd -f -u rspamd -g mail -autostart=true -autorestart=true -priority=11 -stdout_logfile=/var/log/happydeliver/rspamd.log -stderr_logfile=/var/log/happydeliver/rspamd_error.log -user=root - # SpamAssassin daemon [program:spamd] command=/usr/sbin/spamd --max-children 5 --helper-home-dir /var/lib/spamassassin --syslog stderr --pidfile /var/run/spamd.pid diff --git a/go.mod b/go.mod index 038eb22..db2ac1d 100644 --- a/go.mod +++ b/go.mod @@ -1,42 +1,41 @@ module git.happydns.org/happyDeliver -go 1.25.0 +go 1.24.6 require ( github.com/JGLTechnologies/gin-rate-limit v1.5.6 github.com/emersion/go-smtp v0.24.0 github.com/getkin/kin-openapi v0.133.0 - github.com/gin-gonic/gin v1.12.0 + github.com/gin-gonic/gin v1.11.0 github.com/google/uuid v1.6.0 - github.com/oapi-codegen/runtime v1.3.0 - golang.org/x/net v0.52.0 + github.com/oapi-codegen/runtime v1.1.2 + golang.org/x/net v0.46.0 gorm.io/driver/postgres v1.6.0 gorm.io/driver/sqlite v1.6.0 - gorm.io/gorm v1.31.1 + gorm.io/gorm v1.31.0 ) require ( github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect - github.com/bytedance/gopkg v0.1.3 // indirect - github.com/bytedance/sonic v1.15.0 // indirect - github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/bytedance/sonic v1.14.0 // indirect + github.com/bytedance/sonic/loader v0.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dprotaso/go-yit v0.0.0-20220510233725-9ba8df137936 // indirect github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect - github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gin-contrib/sse v1.1.0 // indirect - github.com/go-openapi/jsonpointer v0.22.4 // indirect - github.com/go-openapi/swag/jsonname v0.25.4 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.30.1 // indirect + github.com/go-playground/validator/v10 v10.27.0 // indirect github.com/goccy/go-json v0.10.5 // indirect - github.com/goccy/go-yaml v1.19.2 // indirect + github.com/goccy/go-yaml v1.18.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgx/v5 v5.8.0 // indirect + github.com/jackc/pgx/v5 v5.7.6 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect @@ -44,35 +43,35 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/mailru/easyjson v0.9.1 // indirect + github.com/mailru/easyjson v0.7.7 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-sqlite3 v1.14.33 // indirect + github.com/mattn/go-sqlite3 v1.14.32 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect - github.com/oapi-codegen/oapi-codegen/v2 v2.5.1 // indirect + github.com/oapi-codegen/oapi-codegen/v2 v2.5.0 // indirect github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 // indirect github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/perimeterx/marshmallow v1.1.5 // indirect - github.com/quic-go/qpack v0.6.0 // indirect - github.com/quic-go/quic-go v0.59.0 // indirect - github.com/redis/go-redis/v9 v9.17.2 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.54.1 // indirect + github.com/redis/go-redis/v9 v9.7.3 // indirect github.com/speakeasy-api/jsonpath v0.6.0 // indirect github.com/speakeasy-api/openapi-overlay v0.10.2 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.3.1 // indirect + github.com/ugorji/go/codec v1.3.0 // indirect github.com/vmware-labs/yaml-jsonpath v0.3.2 // indirect - github.com/woodsbury/decimal128 v1.4.0 // indirect - go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect - golang.org/x/arch v0.23.0 // indirect - golang.org/x/crypto v0.49.0 // indirect - golang.org/x/mod v0.33.0 // indirect - golang.org/x/sync v0.20.0 // indirect - golang.org/x/sys v0.42.0 // indirect - golang.org/x/text v0.35.0 // indirect - golang.org/x/tools v0.42.0 // indirect - google.golang.org/protobuf v1.36.11 // indirect + github.com/woodsbury/decimal128 v1.3.0 // indirect + go.uber.org/mock v0.5.0 // indirect + golang.org/x/arch v0.20.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/mod v0.28.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/tools v0.37.0 // indirect + google.golang.org/protobuf v1.36.9 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 10c9b72..266785d 100644 --- a/go.sum +++ b/go.sum @@ -8,12 +8,10 @@ github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs= github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c= github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA= github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0= -github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= -github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= -github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= -github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= -github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= -github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ= +github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA= +github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA= +github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -36,35 +34,33 @@ github.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4Mc github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= -github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= -github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ= github.com/getkin/kin-openapi v0.133.0/go.mod h1:boAciF6cXk5FhPqe/NQeBTeenbjqU4LhWBf09ILVvWE= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= -github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= -github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= -github.com/go-openapi/jsonpointer v0.22.4 h1:dZtK82WlNpVLDW2jlA1YCiVJFVqkED1MegOUy9kR5T4= -github.com/go-openapi/jsonpointer v0.22.4/go.mod h1:elX9+UgznpFhgBuaMQ7iu4lvvX1nvNsesQ3oxmYTw80= -github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI= -github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag= -github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls= -github.com/go-openapi/testify/v2 v2.0.2/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk= +github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= -github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= +github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= -github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= -github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= +github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= @@ -90,8 +86,8 @@ github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsI github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo= -github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw= +github.com/jackc/pgx/v5 v5.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk= +github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= @@ -114,12 +110,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/mailru/easyjson v0.9.1 h1:LbtsOm5WAswyWbvTEOqhypdPeZzHavpZx96/n553mR8= -github.com/mailru/easyjson v0.9.1/go.mod h1:1+xMtQp2MRNVL/V1bOzuP3aP8VNwRW55fQUto+XFtTU= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.33 h1:A5blZ5ulQo2AtayQ9/limgHEkFreKj1Dv226a1K73s0= -github.com/mattn/go-sqlite3 v1.14.33/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs= +github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -130,10 +126,10 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwd github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/oapi-codegen/oapi-codegen/v2 v2.5.1 h1:5vHNY1uuPBRBWqB2Dp0G7YB03phxLQZupZTIZaeorjc= -github.com/oapi-codegen/oapi-codegen/v2 v2.5.1/go.mod h1:ro0npU1BWkcGpCgGD9QwPp44l5OIZ94tB3eabnT7DjQ= -github.com/oapi-codegen/runtime v1.3.0 h1:vyK1zc0gDWWXgk2xoQa4+X4RNNc5SL2RbTpJS/4vMYA= -github.com/oapi-codegen/runtime v1.3.0/go.mod h1:kOdeacKy7t40Rclb1je37ZLFboFxh+YLy0zaPCMibPY= +github.com/oapi-codegen/oapi-codegen/v2 v2.5.0 h1:iJvF8SdB/3/+eGOXEpsWkD8FQAHj6mqkb6Fnsoc8MFU= +github.com/oapi-codegen/oapi-codegen/v2 v2.5.0/go.mod h1:fwlMxUEMuQK5ih9aymrxKPQqNm2n8bdLk1ppjH+lr9w= +github.com/oapi-codegen/runtime v1.1.2 h1:P2+CubHq8fO4Q6fV1tqDBZHCwpVpvPg7oKiYzQgXIyI= +github.com/oapi-codegen/runtime v1.1.2/go.mod h1:SK9X900oXmPWilYR5/WKPzt3Kqxn/uS/+lbpREv+eCg= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037 h1:G7ERwszslrBzRxj//JalHPu/3yz+De2J+4aLtSRlHiY= github.com/oasdiff/yaml v0.0.0-20250309154309-f31be36b4037/go.mod h1:2bpvgLBZEtENV5scfDFEtB/5+1M4hkQhDQrccEJ/qGw= github.com/oasdiff/yaml3 v0.0.0-20250309153720-d2182401db90 h1:bQx3WeLcUWy+RletIKwUIt4x3t8n2SxavmoclizMb8c= @@ -156,12 +152,12 @@ github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= -github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= -github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= -github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= -github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= -github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= +github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= +github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= +github.com/quic-go/quic-go v0.54.1 h1:4ZAWm0AhCb6+hE+l5Q1NAL0iRn/ZrMwqHRGQiFwj2eg= +github.com/quic-go/quic-go v0.54.1/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= +github.com/redis/go-redis/v9 v9.7.3 h1:YpPyAayJV+XErNsatSElgRZZVCwXX9QzkKYNvO7x0wM= +github.com/redis/go-redis/v9 v9.7.3/go.mod h1:bGUrSggJ9X9GUmZpZNEOQKaANxSGgOEBRltRTZHSvrA= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= @@ -174,40 +170,36 @@ github.com/spkg/bom v0.0.0-20160624110644-59b7046e48ad/go.mod h1:qLr4V1qq6nMqFKk github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= -github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA= +github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= github.com/vmware-labs/yaml-jsonpath v0.3.2 h1:/5QKeCBGdsInyDCyVNLbXyilb61MXGi9NP674f9Hobk= github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN1BWz5V+51Ewf8k+rQ= -github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= -github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= +github.com/woodsbury/decimal128 v1.3.0 h1:8pffMNWIlC0O5vbyHWFZAt5yWvWcrHA+3ovIIjVWss0= +github.com/woodsbury/decimal128 v1.3.0/go.mod h1:C5UTmyTjW3JftjUFzOVhC20BEQa2a4ZKOB5I6Zjb+ds= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= -go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= -go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= -go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= -golang.org/x/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg= -golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A= +go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU= +go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM= +golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c= +golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= -golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U= +golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -215,13 +207,13 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= -golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -237,21 +229,21 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k= -golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0= +golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= +golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -264,8 +256,8 @@ google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzi google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= -google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= +google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -287,5 +279,5 @@ gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4= gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo= gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= -gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= -gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY= +gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= diff --git a/internal/api/handlers.go b/internal/api/handlers.go index 470136e..7489f99 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -40,8 +40,6 @@ import ( // This interface breaks the circular dependency with pkg/analyzer type EmailAnalyzer interface { AnalyzeEmailBytes(rawEmail []byte, testID uuid.UUID) (reportJSON []byte, err error) - AnalyzeDomain(domain string) (dnsResults *DNSResults, score int, grade string) - CheckBlacklistIP(ip string) (checks []BlacklistCheck, whitelists []BlacklistCheck, listedCount int, score int, grade string, err error) } // APIHandler implements the ServerInterface for handling API requests @@ -292,92 +290,3 @@ func (h *APIHandler) GetStatus(c *gin.Context) { Uptime: &uptime, }) } - -// TestDomain performs synchronous domain analysis -// (POST /domain) -func (h *APIHandler) TestDomain(c *gin.Context) { - var request DomainTestRequest - - // Bind and validate request - if err := c.ShouldBindJSON(&request); err != nil { - c.JSON(http.StatusBadRequest, Error{ - Error: "invalid_request", - Message: "Invalid request body", - Details: stringPtr(err.Error()), - }) - return - } - - // Perform domain analysis - dnsResults, score, grade := h.analyzer.AnalyzeDomain(request.Domain) - - // Convert grade string to DomainTestResponseGrade enum - var responseGrade DomainTestResponseGrade - switch grade { - case "A+": - responseGrade = DomainTestResponseGradeA - case "A": - responseGrade = DomainTestResponseGradeA1 - case "B": - responseGrade = DomainTestResponseGradeB - case "C": - responseGrade = DomainTestResponseGradeC - case "D": - responseGrade = DomainTestResponseGradeD - case "E": - responseGrade = DomainTestResponseGradeE - case "F": - responseGrade = DomainTestResponseGradeF - default: - responseGrade = DomainTestResponseGradeF - } - - // Build response - response := DomainTestResponse{ - Domain: request.Domain, - Score: score, - Grade: responseGrade, - DnsResults: *dnsResults, - } - - c.JSON(http.StatusOK, response) -} - -// CheckBlacklist checks an IP address against DNS blacklists -// (POST /blacklist) -func (h *APIHandler) CheckBlacklist(c *gin.Context) { - var request BlacklistCheckRequest - - // Bind and validate request - if err := c.ShouldBindJSON(&request); err != nil { - c.JSON(http.StatusBadRequest, Error{ - Error: "invalid_request", - Message: "Invalid request body", - Details: stringPtr(err.Error()), - }) - return - } - - // Perform blacklist check using analyzer - checks, whitelists, listedCount, score, grade, err := h.analyzer.CheckBlacklistIP(request.Ip) - if err != nil { - c.JSON(http.StatusBadRequest, Error{ - Error: "invalid_ip", - Message: "Invalid IP address", - Details: stringPtr(err.Error()), - }) - return - } - - // Build response - response := BlacklistCheckResponse{ - Ip: request.Ip, - Blacklists: checks, - Whitelists: &whitelists, - ListedCount: listedCount, - Score: score, - Grade: BlacklistCheckResponseGrade(grade), - } - - c.JSON(http.StatusOK, response) -} diff --git a/internal/app/cli_backup.go b/internal/app/cli_backup.go deleted file mode 100644 index 4b01fbb..0000000 --- a/internal/app/cli_backup.go +++ /dev/null @@ -1,156 +0,0 @@ -// This file is part of the happyDeliver (R) project. -// Copyright (c) 2025 happyDomain -// Authors: Pierre-Olivier Mercier, et al. -// -// This program is offered under a commercial and under the AGPL license. -// For commercial licensing, contact us at . -// -// For AGPL licensing: -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package app - -import ( - "encoding/json" - "fmt" - "io" - "os" - - "git.happydns.org/happyDeliver/internal/config" - "git.happydns.org/happyDeliver/internal/storage" -) - -// BackupData represents the structure of a backup file -type BackupData struct { - Version string `json:"version"` - Reports []storage.Report `json:"reports"` -} - -// RunBackup exports the database to stdout as JSON -func RunBackup(cfg *config.Config) error { - if err := cfg.Validate(); err != nil { - return err - } - - // Initialize storage - store, err := storage.NewStorage(cfg.Database.Type, cfg.Database.DSN) - if err != nil { - return fmt.Errorf("failed to connect to database: %w", err) - } - defer store.Close() - - fmt.Fprintf(os.Stderr, "Connected to %s database\n", cfg.Database.Type) - - // Get all reports from the database - reports, err := storage.GetAllReports(store) - if err != nil { - return fmt.Errorf("failed to retrieve reports: %w", err) - } - - fmt.Fprintf(os.Stderr, "Found %d reports to backup\n", len(reports)) - - // Create backup data structure - backup := BackupData{ - Version: "1.0", - Reports: reports, - } - - // Encode to JSON and write to stdout - encoder := json.NewEncoder(os.Stdout) - encoder.SetIndent("", " ") - if err := encoder.Encode(backup); err != nil { - return fmt.Errorf("failed to encode backup data: %w", err) - } - - return nil -} - -// RunRestore imports the database from a JSON file or stdin -func RunRestore(cfg *config.Config, inputPath string) error { - if err := cfg.Validate(); err != nil { - return err - } - - // Determine input source - var reader io.Reader - if inputPath == "" || inputPath == "-" { - fmt.Fprintln(os.Stderr, "Reading backup from stdin...") - reader = os.Stdin - } else { - inFile, err := os.Open(inputPath) - if err != nil { - return fmt.Errorf("failed to open backup file: %w", err) - } - defer inFile.Close() - fmt.Fprintf(os.Stderr, "Reading backup from file: %s\n", inputPath) - reader = inFile - } - - // Decode JSON - var backup BackupData - decoder := json.NewDecoder(reader) - if err := decoder.Decode(&backup); err != nil { - if err == io.EOF { - return fmt.Errorf("backup file is empty or corrupted") - } - return fmt.Errorf("failed to decode backup data: %w", err) - } - - fmt.Fprintf(os.Stderr, "Backup version: %s\n", backup.Version) - fmt.Fprintf(os.Stderr, "Found %d reports in backup\n", len(backup.Reports)) - - // Initialize storage - store, err := storage.NewStorage(cfg.Database.Type, cfg.Database.DSN) - if err != nil { - return fmt.Errorf("failed to connect to database: %w", err) - } - defer store.Close() - - fmt.Fprintf(os.Stderr, "Connected to %s database\n", cfg.Database.Type) - - // Restore reports - restored, skipped, failed := 0, 0, 0 - for _, report := range backup.Reports { - // Check if report already exists - exists, err := store.ReportExists(report.TestID) - if err != nil { - fmt.Fprintf(os.Stderr, "Warning: Failed to check if report %s exists: %v\n", report.TestID, err) - failed++ - continue - } - - if exists { - fmt.Fprintf(os.Stderr, "Report %s already exists, skipping\n", report.TestID) - skipped++ - continue - } - - // Create the report - _, err = storage.CreateReportFromBackup(store, &report) - if err != nil { - fmt.Fprintf(os.Stderr, "Warning: Failed to restore report %s: %v\n", report.TestID, err) - failed++ - continue - } - - restored++ - } - - fmt.Fprintf(os.Stderr, "Restore completed: %d restored, %d skipped, %d failed\n", restored, skipped, failed) - if failed > 0 { - return fmt.Errorf("restore completed with %d failures", failed) - } - - return nil -} diff --git a/internal/config/cli.go b/internal/config/cli.go index 77108ca..2a61bad 100644 --- a/internal/config/cli.go +++ b/internal/config/cli.go @@ -34,16 +34,13 @@ func declareFlags(o *Config) { flag.StringVar(&o.Email.Domain, "domain", o.Email.Domain, "Domain used to receive emails") flag.StringVar(&o.Email.TestAddressPrefix, "address-prefix", o.Email.TestAddressPrefix, "Expected email adress prefix (deny address that doesn't start with this prefix)") flag.StringVar(&o.Email.LMTPAddr, "lmtp-addr", o.Email.LMTPAddr, "LMTP server listen address") - flag.StringVar(&o.Email.ReceiverHostname, "receiver-hostname", o.Email.ReceiverHostname, "Hostname used to filter Authentication-Results headers (defaults to os.Hostname())") flag.DurationVar(&o.Analysis.DNSTimeout, "dns-timeout", o.Analysis.DNSTimeout, "Timeout when performing DNS query") flag.DurationVar(&o.Analysis.HTTPTimeout, "http-timeout", o.Analysis.HTTPTimeout, "Timeout when performing HTTP query") flag.Var(&StringArray{&o.Analysis.RBLs}, "rbl", "Append a RBL (use this option multiple time to append multiple RBLs)") flag.BoolVar(&o.Analysis.CheckAllIPs, "check-all-ips", o.Analysis.CheckAllIPs, "Check all IPs found in email headers against RBLs (not just the first one)") - flag.StringVar(&o.Analysis.RspamdAPIURL, "rspamd-api-url", o.Analysis.RspamdAPIURL, "rspamd API URL for symbol descriptions (default: use embedded list)") flag.DurationVar(&o.ReportRetention, "report-retention", o.ReportRetention, "How long to keep reports (e.g., 720h, 30d). 0 = keep forever") flag.UintVar(&o.RateLimit, "rate-limit", o.RateLimit, "API rate limit (requests per second per IP)") flag.Var(&URL{&o.SurveyURL}, "survey-url", "URL for user feedback survey") - flag.StringVar(&o.CustomLogoURL, "custom-logo-url", o.CustomLogoURL, "URL for custom logo image in the web UI") // Others flags are declared in some other files likes sources, storages, ... when they need specials configurations } diff --git a/internal/config/config.go b/internal/config/config.go index 9d803d0..be5e63a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -34,11 +34,6 @@ import ( openapi_types "github.com/oapi-codegen/runtime/types" ) -func getHostname() string { - h, _ := os.Hostname() - return h -} - // Config represents the application configuration type Config struct { DevProxy string @@ -49,7 +44,6 @@ type Config struct { ReportRetention time.Duration // How long to keep reports. 0 = keep forever RateLimit uint // API rate limit (requests per second per IP) SurveyURL url.URL // URL for user feedback survey - CustomLogoURL string // URL for custom logo image in the web UI } // DatabaseConfig contains database connection settings @@ -63,7 +57,6 @@ type EmailConfig struct { Domain string TestAddressPrefix string LMTPAddr string - ReceiverHostname string } // AnalysisConfig contains timeout and behavior settings for email analysis @@ -71,9 +64,7 @@ type AnalysisConfig struct { DNSTimeout time.Duration HTTPTimeout time.Duration RBLs []string - DNSWLs []string - CheckAllIPs bool // Check all IPs found in headers, not just the first one - RspamdAPIURL string // rspamd API URL for fetching symbol descriptions (empty = use embedded list) + CheckAllIPs bool // Check all IPs found in headers, not just the first one } // DefaultConfig returns a configuration with sensible defaults @@ -91,13 +82,11 @@ func DefaultConfig() *Config { Domain: "happydeliver.local", TestAddressPrefix: "test-", LMTPAddr: "127.0.0.1:2525", - ReceiverHostname: getHostname(), }, Analysis: AnalysisConfig{ DNSTimeout: 5 * time.Second, HTTPTimeout: 10 * time.Second, RBLs: []string{}, - DNSWLs: []string{}, CheckAllIPs: false, // By default, only check the first IP }, } diff --git a/internal/receiver/receiver.go b/internal/receiver/receiver.go index f06f535..062a091 100644 --- a/internal/receiver/receiver.go +++ b/internal/receiver/receiver.go @@ -98,17 +98,6 @@ func (r *EmailReceiver) ProcessEmailBytes(rawEmail []byte, recipientEmail string log.Printf("Analysis complete. Grade: %s. Score: %d/100", result.Report.Grade, result.Report.Score) - // Warn if the last Received hop doesn't match the expected receiver hostname - if r.config.Email.ReceiverHostname != "" && - result.Report.HeaderAnalysis != nil && - result.Report.HeaderAnalysis.ReceivedChain != nil && - len(*result.Report.HeaderAnalysis.ReceivedChain) > 0 { - lastHop := (*result.Report.HeaderAnalysis.ReceivedChain)[0] - if lastHop.By != nil && *lastHop.By != r.config.Email.ReceiverHostname { - log.Printf("WARNING: Last Received hop 'by' field (%s) does not match expected receiver hostname (%s): check your RECEIVER_HOSTNAME config as authentication results will be false", *lastHop.By, r.config.Email.ReceiverHostname) - } - } - // Marshal report to JSON reportJSON, err := json.Marshal(result.Report) if err != nil { diff --git a/internal/storage/storage.go b/internal/storage/storage.go index 39b2eb6..35aa0df 100644 --- a/internal/storage/storage.go +++ b/internal/storage/storage.go @@ -147,33 +147,3 @@ func (s *DBStorage) Close() error { } return sqlDB.Close() } - -// GetAllReports retrieves all reports from the database -func GetAllReports(s Storage) ([]Report, error) { - dbStorage, ok := s.(*DBStorage) - if !ok { - return nil, fmt.Errorf("storage type does not support GetAllReports") - } - - var reports []Report - if err := dbStorage.db.Find(&reports).Error; err != nil { - return nil, fmt.Errorf("failed to retrieve reports: %w", err) - } - - return reports, nil -} - -// CreateReportFromBackup creates a report from backup data, preserving timestamps -func CreateReportFromBackup(s Storage, report *Report) (*Report, error) { - dbStorage, ok := s.(*DBStorage) - if !ok { - return nil, fmt.Errorf("storage type does not support CreateReportFromBackup") - } - - // Use Create to insert the report with all fields including timestamps - if err := dbStorage.db.Create(report).Error; err != nil { - return nil, fmt.Errorf("failed to create report from backup: %w", err) - } - - return report, nil -} diff --git a/pkg/analyzer/analyzer.go b/pkg/analyzer/analyzer.go index f21d1f8..99b7b52 100644 --- a/pkg/analyzer/analyzer.go +++ b/pkg/analyzer/analyzer.go @@ -41,13 +41,10 @@ type EmailAnalyzer struct { // NewEmailAnalyzer creates a new email analyzer with the given configuration func NewEmailAnalyzer(cfg *config.Config) *EmailAnalyzer { generator := NewReportGenerator( - cfg.Email.ReceiverHostname, cfg.Analysis.DNSTimeout, cfg.Analysis.HTTPTimeout, cfg.Analysis.RBLs, - cfg.Analysis.DNSWLs, cfg.Analysis.CheckAllIPs, - cfg.Analysis.RspamdAPIURL, ) return &EmailAnalyzer{ @@ -111,40 +108,3 @@ func (a *APIAdapter) AnalyzeEmailBytes(rawEmail []byte, testID uuid.UUID) ([]byt return reportJSON, nil } - -// AnalyzeDomain performs DNS analysis for a domain and returns the results -func (a *APIAdapter) AnalyzeDomain(domain string) (*api.DNSResults, int, string) { - // Perform DNS analysis - dnsResults := a.analyzer.generator.dnsAnalyzer.AnalyzeDomainOnly(domain) - - // Calculate score - score, grade := a.analyzer.generator.dnsAnalyzer.CalculateDomainOnlyScore(dnsResults) - - return dnsResults, score, grade -} - -// CheckBlacklistIP checks a single IP address against DNS blacklists and whitelists -func (a *APIAdapter) CheckBlacklistIP(ip string) ([]api.BlacklistCheck, []api.BlacklistCheck, int, int, string, error) { - // Check the IP against all configured RBLs - checks, listedCount, err := a.analyzer.generator.rblChecker.CheckIP(ip) - if err != nil { - return nil, nil, 0, 0, "", err - } - - // Calculate score using the existing function - // Create a minimal RBLResults structure for scoring - results := &DNSListResults{ - Checks: map[string][]api.BlacklistCheck{ip: checks}, - IPsChecked: []string{ip}, - ListedCount: listedCount, - } - score, grade := a.analyzer.generator.rblChecker.CalculateScore(results, false) - - // Check the IP against all configured DNSWLs (informational only) - whitelists, _, err := a.analyzer.generator.dnswlChecker.CheckIP(ip) - if err != nil { - whitelists = nil - } - - return checks, whitelists, listedCount, score, grade, nil -} diff --git a/pkg/analyzer/authentication.go b/pkg/analyzer/authentication.go index 2beeb1f..02f8b28 100644 --- a/pkg/analyzer/authentication.go +++ b/pkg/analyzer/authentication.go @@ -28,13 +28,11 @@ import ( ) // AuthenticationAnalyzer analyzes email authentication results -type AuthenticationAnalyzer struct { - receiverHostname string -} +type AuthenticationAnalyzer struct{} // NewAuthenticationAnalyzer creates a new authentication analyzer -func NewAuthenticationAnalyzer(receiverHostname string) *AuthenticationAnalyzer { - return &AuthenticationAnalyzer{receiverHostname: receiverHostname} +func NewAuthenticationAnalyzer() *AuthenticationAnalyzer { + return &AuthenticationAnalyzer{} } // AnalyzeAuthentication extracts and analyzes authentication results from email headers @@ -42,7 +40,7 @@ func (a *AuthenticationAnalyzer) AnalyzeAuthentication(email *EmailMessage) *api results := &api.AuthenticationResults{} // Parse Authentication-Results headers - authHeaders := email.GetAuthenticationResults(a.receiverHostname) + authHeaders := email.GetAuthenticationResults() for _, header := range authHeaders { a.parseAuthenticationResultsHeader(header, results) } @@ -52,6 +50,13 @@ func (a *AuthenticationAnalyzer) AnalyzeAuthentication(email *EmailMessage) *api results.Spf = a.parseLegacySPF(email) } + if results.Dkim == nil || len(*results.Dkim) == 0 { + dkimResults := a.parseLegacyDKIM(email) + if len(dkimResults) > 0 { + results.Dkim = &dkimResults + } + } + // Parse ARC headers if not already parsed from Authentication-Results if results.Arc == nil { results.Arc = a.parseARCHeaders(email) @@ -152,32 +157,27 @@ func (a *AuthenticationAnalyzer) CalculateAuthenticationScore(results *api.Authe score := 0 - // Core authentication (90 points total) - // SPF (30 points) - score += 30 * a.calculateSPFScore(results) / 100 + // IPRev (15 points) + score += 15 * a.calculateIPRevScore(results) / 100 - // DKIM (30 points) - score += 30 * a.calculateDKIMScore(results) / 100 + // SPF (25 points) + score += 25 * a.calculateSPFScore(results) / 100 - // DMARC (30 points) - score += 30 * a.calculateDMARCScore(results) / 100 + // DKIM (23 points) + score += 23 * a.calculateDKIMScore(results) / 100 + + // X-Google-DKIM (optional) - penalty if failed + score += 12 * a.calculateXGoogleDKIMScore(results) / 100 + + // X-Aligned-From + score += 2 * a.calculateXAlignedFromScore(results) / 100 + + // DMARC (25 points) + score += 25 * a.calculateDMARCScore(results) / 100 // BIMI (10 points) score += 10 * a.calculateBIMIScore(results) / 100 - // Penalty-only: IPRev (up to -7 points on failure) - if iprevScore := a.calculateIPRevScore(results); iprevScore < 100 { - score += 7 * (iprevScore - 100) / 100 - } - - // Penalty-only: X-Google-DKIM (up to -12 points on failure) - score += 12 * a.calculateXGoogleDKIMScore(results) / 100 - - // Penalty-only: X-Aligned-From (up to -5 points on failure) - if xAlignedScore := a.calculateXAlignedFromScore(results); xAlignedScore < 100 { - score += 5 * (xAlignedScore - 100) / 100 - } - // Ensure score doesn't exceed 100 if score > 100 { score = 100 diff --git a/pkg/analyzer/authentication_arc_test.go b/pkg/analyzer/authentication_arc_test.go index 7f2f99e..9269d70 100644 --- a/pkg/analyzer/authentication_arc_test.go +++ b/pkg/analyzer/authentication_arc_test.go @@ -50,7 +50,7 @@ func TestParseARCResult(t *testing.T) { }, } - analyzer := NewAuthenticationAnalyzer("") + analyzer := NewAuthenticationAnalyzer() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -136,7 +136,7 @@ func TestValidateARCChain(t *testing.T) { }, } - analyzer := NewAuthenticationAnalyzer("") + analyzer := NewAuthenticationAnalyzer() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/analyzer/authentication_bimi_test.go b/pkg/analyzer/authentication_bimi_test.go index 7cb9c85..b1b5468 100644 --- a/pkg/analyzer/authentication_bimi_test.go +++ b/pkg/analyzer/authentication_bimi_test.go @@ -64,7 +64,7 @@ func TestParseBIMIResult(t *testing.T) { }, } - analyzer := NewAuthenticationAnalyzer("") + analyzer := NewAuthenticationAnalyzer() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/analyzer/authentication_dkim.go b/pkg/analyzer/authentication_dkim.go index b6cf5f8..9f1774b 100644 --- a/pkg/analyzer/authentication_dkim.go +++ b/pkg/analyzer/authentication_dkim.go @@ -59,6 +59,40 @@ func (a *AuthenticationAnalyzer) parseDKIMResult(part string) *api.AuthResult { return result } +// parseLegacyDKIM attempts to parse DKIM from DKIM-Signature header +func (a *AuthenticationAnalyzer) parseLegacyDKIM(email *EmailMessage) []api.AuthResult { + var results []api.AuthResult + + // Get all DKIM-Signature headers + dkimHeaders := email.Header[textprotoCanonical("DKIM-Signature")] + for _, dkimHeader := range dkimHeaders { + result := api.AuthResult{ + Result: api.AuthResultResultNone, // We can't determine pass/fail from signature alone + } + + // Extract domain (d=) + domainRe := regexp.MustCompile(`d=([^\s;]+)`) + if matches := domainRe.FindStringSubmatch(dkimHeader); len(matches) > 1 { + domain := matches[1] + result.Domain = &domain + } + + // Extract selector (s=) + selectorRe := regexp.MustCompile(`s=([^\s;]+)`) + if matches := selectorRe.FindStringSubmatch(dkimHeader); len(matches) > 1 { + selector := matches[1] + result.Selector = &selector + } + + details := "DKIM signature present (verification status unknown)" + result.Details = &details + + results = append(results, result) + } + + return results +} + func (a *AuthenticationAnalyzer) calculateDKIMScore(results *api.AuthenticationResults) (score int) { // Expect at least one passing signature if results.Dkim != nil && len(*results.Dkim) > 0 { diff --git a/pkg/analyzer/authentication_dkim_test.go b/pkg/analyzer/authentication_dkim_test.go index 3218639..323e421 100644 --- a/pkg/analyzer/authentication_dkim_test.go +++ b/pkg/analyzer/authentication_dkim_test.go @@ -22,6 +22,7 @@ package analyzer import ( + "strings" "testing" "git.happydns.org/happyDeliver/internal/api" @@ -58,7 +59,7 @@ func TestParseDKIMResult(t *testing.T) { }, } - analyzer := NewAuthenticationAnalyzer("") + analyzer := NewAuthenticationAnalyzer() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -84,3 +85,246 @@ func TestParseDKIMResult(t *testing.T) { }) } } + +func TestParseLegacyDKIM(t *testing.T) { + tests := []struct { + name string + dkimSignatures []string + expectedCount int + expectedDomains []string + expectedSelector []string + }{ + { + name: "Single DKIM signature with domain and selector", + dkimSignatures: []string{ + "v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=selector1; h=from:to:subject:date; bh=xyz; b=abc", + }, + expectedCount: 1, + expectedDomains: []string{"example.com"}, + expectedSelector: []string{"selector1"}, + }, + { + name: "Multiple DKIM signatures", + dkimSignatures: []string{ + "v=1; a=rsa-sha256; d=example.com; s=selector1; b=abc123", + "v=1; a=rsa-sha256; d=example.com; s=selector2; b=def456", + }, + expectedCount: 2, + expectedDomains: []string{"example.com", "example.com"}, + expectedSelector: []string{"selector1", "selector2"}, + }, + { + name: "DKIM signature with different domain", + dkimSignatures: []string{ + "v=1; a=rsa-sha256; d=mail.example.org; s=default; b=xyz789", + }, + expectedCount: 1, + expectedDomains: []string{"mail.example.org"}, + expectedSelector: []string{"default"}, + }, + { + name: "DKIM signature with subdomain", + dkimSignatures: []string{ + "v=1; a=rsa-sha256; d=newsletters.example.com; s=marketing; b=aaa", + }, + expectedCount: 1, + expectedDomains: []string{"newsletters.example.com"}, + expectedSelector: []string{"marketing"}, + }, + { + name: "Multiple signatures from different domains", + dkimSignatures: []string{ + "v=1; a=rsa-sha256; d=example.com; s=s1; b=abc", + "v=1; a=rsa-sha256; d=relay.com; s=s2; b=def", + }, + expectedCount: 2, + expectedDomains: []string{"example.com", "relay.com"}, + expectedSelector: []string{"s1", "s2"}, + }, + { + name: "No DKIM signatures", + dkimSignatures: []string{}, + expectedCount: 0, + expectedDomains: []string{}, + expectedSelector: []string{}, + }, + { + name: "DKIM signature without selector", + dkimSignatures: []string{ + "v=1; a=rsa-sha256; d=example.com; b=abc123", + }, + expectedCount: 1, + expectedDomains: []string{"example.com"}, + expectedSelector: []string{""}, + }, + { + name: "DKIM signature without domain", + dkimSignatures: []string{ + "v=1; a=rsa-sha256; s=selector1; b=abc123", + }, + expectedCount: 1, + expectedDomains: []string{""}, + expectedSelector: []string{"selector1"}, + }, + { + name: "DKIM signature with whitespace in parameters", + dkimSignatures: []string{ + "v=1; a=rsa-sha256; d=example.com ; s=selector1 ; b=abc123", + }, + expectedCount: 1, + expectedDomains: []string{"example.com"}, + expectedSelector: []string{"selector1"}, + }, + { + name: "DKIM signature with multiline format", + dkimSignatures: []string{ + "v=1; a=rsa-sha256; c=relaxed/relaxed;\r\n\td=example.com; s=selector1;\r\n\th=from:to:subject:date;\r\n\tb=abc123def456ghi789", + }, + expectedCount: 1, + expectedDomains: []string{"example.com"}, + expectedSelector: []string{"selector1"}, + }, + { + name: "DKIM signature with ed25519 algorithm", + dkimSignatures: []string{ + "v=1; a=ed25519-sha256; d=example.com; s=ed25519; b=xyz", + }, + expectedCount: 1, + expectedDomains: []string{"example.com"}, + expectedSelector: []string{"ed25519"}, + }, + { + name: "Complex real-world DKIM signature", + dkimSignatures: []string{ + "v=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=20230601; t=1234567890; x=1234567950; darn=example.com; h=to:subject:message-id:date:from:mime-version:from:to:cc:subject:date:message-id:reply-to; bh=abc123def456==; b=longsignaturehere==", + }, + expectedCount: 1, + expectedDomains: []string{"google.com"}, + expectedSelector: []string{"20230601"}, + }, + } + + analyzer := NewAuthenticationAnalyzer() + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a mock email message with DKIM-Signature headers + email := &EmailMessage{ + Header: make(map[string][]string), + } + if len(tt.dkimSignatures) > 0 { + email.Header["Dkim-Signature"] = tt.dkimSignatures + } + + results := analyzer.parseLegacyDKIM(email) + + // Check count + if len(results) != tt.expectedCount { + t.Errorf("Expected %d results, got %d", tt.expectedCount, len(results)) + return + } + + // Check each result + for i, result := range results { + // All legacy DKIM results should have Result = none + if result.Result != api.AuthResultResultNone { + t.Errorf("Result[%d].Result = %v, want %v", i, result.Result, api.AuthResultResultNone) + } + + // Check domain + if i < len(tt.expectedDomains) { + expectedDomain := tt.expectedDomains[i] + if expectedDomain != "" { + if result.Domain == nil { + t.Errorf("Result[%d].Domain = nil, want %v", i, expectedDomain) + } else if strings.TrimSpace(*result.Domain) != expectedDomain { + t.Errorf("Result[%d].Domain = %v, want %v", i, *result.Domain, expectedDomain) + } + } + } + + // Check selector + if i < len(tt.expectedSelector) { + expectedSelector := tt.expectedSelector[i] + if expectedSelector != "" { + if result.Selector == nil { + t.Errorf("Result[%d].Selector = nil, want %v", i, expectedSelector) + } else if strings.TrimSpace(*result.Selector) != expectedSelector { + t.Errorf("Result[%d].Selector = %v, want %v", i, *result.Selector, expectedSelector) + } + } + } + + // Check that Details is set + if result.Details == nil { + t.Errorf("Result[%d].Details = nil, expected non-nil", i) + } else { + expectedDetails := "DKIM signature present (verification status unknown)" + if *result.Details != expectedDetails { + t.Errorf("Result[%d].Details = %v, want %v", i, *result.Details, expectedDetails) + } + } + } + }) + } +} + +func TestParseLegacyDKIM_Integration(t *testing.T) { + hostname = "" + + // Test that parseLegacyDKIM is properly integrated into AnalyzeAuthentication + t.Run("Legacy DKIM is used when no Authentication-Results", func(t *testing.T) { + analyzer := NewAuthenticationAnalyzer() + email := &EmailMessage{ + Header: make(map[string][]string), + } + email.Header["Dkim-Signature"] = []string{ + "v=1; a=rsa-sha256; d=example.com; s=selector1; b=abc", + } + + results := analyzer.AnalyzeAuthentication(email) + + if results.Dkim == nil { + t.Fatal("Expected DKIM results, got nil") + } + if len(*results.Dkim) != 1 { + t.Errorf("Expected 1 DKIM result, got %d", len(*results.Dkim)) + } + if (*results.Dkim)[0].Result != api.AuthResultResultNone { + t.Errorf("Expected DKIM result to be 'none', got %v", (*results.Dkim)[0].Result) + } + if (*results.Dkim)[0].Domain == nil || *(*results.Dkim)[0].Domain != "example.com" { + t.Error("Expected domain to be 'example.com'") + } + }) + + t.Run("Legacy DKIM is NOT used when Authentication-Results present", func(t *testing.T) { + analyzer := NewAuthenticationAnalyzer() + email := &EmailMessage{ + Header: make(map[string][]string), + } + // Both Authentication-Results and DKIM-Signature headers + email.Header["Authentication-Results"] = []string{ + "mx.example.com; dkim=pass header.d=verified.com header.s=s1", + } + email.Header["Dkim-Signature"] = []string{ + "v=1; a=rsa-sha256; d=example.com; s=selector1; b=abc", + } + + results := analyzer.AnalyzeAuthentication(email) + + // Should use the Authentication-Results DKIM (pass from verified.com), not the legacy signature + if results.Dkim == nil { + t.Fatal("Expected DKIM results, got nil") + } + if len(*results.Dkim) != 1 { + t.Errorf("Expected 1 DKIM result, got %d", len(*results.Dkim)) + } + if (*results.Dkim)[0].Result != api.AuthResultResultPass { + t.Errorf("Expected DKIM result to be 'pass', got %v", (*results.Dkim)[0].Result) + } + if (*results.Dkim)[0].Domain == nil || *(*results.Dkim)[0].Domain != "verified.com" { + t.Error("Expected domain to be 'verified.com' from Authentication-Results, not 'example.com' from legacy") + } + }) +} diff --git a/pkg/analyzer/authentication_dmarc_test.go b/pkg/analyzer/authentication_dmarc_test.go index 3b8fb08..d7fda84 100644 --- a/pkg/analyzer/authentication_dmarc_test.go +++ b/pkg/analyzer/authentication_dmarc_test.go @@ -48,7 +48,7 @@ func TestParseDMARCResult(t *testing.T) { }, } - analyzer := NewAuthenticationAnalyzer("") + analyzer := NewAuthenticationAnalyzer() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/analyzer/authentication_iprev.go b/pkg/analyzer/authentication_iprev.go index e799094..6538cbb 100644 --- a/pkg/analyzer/authentication_iprev.go +++ b/pkg/analyzer/authentication_iprev.go @@ -69,5 +69,5 @@ func (a *AuthenticationAnalyzer) calculateIPRevScore(results *api.Authentication } } - return 100 + return 0 } diff --git a/pkg/analyzer/authentication_iprev_test.go b/pkg/analyzer/authentication_iprev_test.go index 5b46995..d0529b5 100644 --- a/pkg/analyzer/authentication_iprev_test.go +++ b/pkg/analyzer/authentication_iprev_test.go @@ -93,7 +93,7 @@ func TestParseIPRevResult(t *testing.T) { }, } - analyzer := NewAuthenticationAnalyzer("") + analyzer := NewAuthenticationAnalyzer() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -181,7 +181,7 @@ func TestParseAuthenticationResultsHeader_IPRev(t *testing.T) { }, } - analyzer := NewAuthenticationAnalyzer("") + analyzer := NewAuthenticationAnalyzer() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/analyzer/authentication_spf.go b/pkg/analyzer/authentication_spf.go index fc41e3c..479c325 100644 --- a/pkg/analyzer/authentication_spf.go +++ b/pkg/analyzer/authentication_spf.go @@ -63,16 +63,6 @@ func (a *AuthenticationAnalyzer) parseLegacySPF(email *EmailMessage) *api.AuthRe return nil } - // Verify receiver matches our hostname - if a.receiverHostname != "" { - receiverRe := regexp.MustCompile(`receiver=([^\s;]+)`) - if matches := receiverRe.FindStringSubmatch(receivedSPF); len(matches) > 1 { - if matches[1] != a.receiverHostname { - return nil - } - } - } - result := &api.AuthResult{} // Extract result (first word) diff --git a/pkg/analyzer/authentication_spf_test.go b/pkg/analyzer/authentication_spf_test.go index 960aef5..7a84c49 100644 --- a/pkg/analyzer/authentication_spf_test.go +++ b/pkg/analyzer/authentication_spf_test.go @@ -60,7 +60,7 @@ func TestParseSPFResult(t *testing.T) { }, } - analyzer := NewAuthenticationAnalyzer("") + analyzer := NewAuthenticationAnalyzer() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -161,7 +161,7 @@ func TestParseLegacySPF(t *testing.T) { }, } - analyzer := NewAuthenticationAnalyzer("") + analyzer := NewAuthenticationAnalyzer() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/analyzer/authentication_test.go b/pkg/analyzer/authentication_test.go index 7122f53..27901b5 100644 --- a/pkg/analyzer/authentication_test.go +++ b/pkg/analyzer/authentication_test.go @@ -100,7 +100,7 @@ func TestGetAuthenticationScore(t *testing.T) { }, } - scorer := NewAuthenticationAnalyzer("") + scorer := NewAuthenticationAnalyzer() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -247,7 +247,7 @@ func TestParseAuthenticationResultsHeader(t *testing.T) { }, } - analyzer := NewAuthenticationAnalyzer("") + analyzer := NewAuthenticationAnalyzer() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -353,7 +353,7 @@ func TestParseAuthenticationResultsHeader(t *testing.T) { func TestParseAuthenticationResultsHeader_OnlyFirstResultParsed(t *testing.T) { // This test verifies that only the first occurrence of each auth method is parsed - analyzer := NewAuthenticationAnalyzer("") + analyzer := NewAuthenticationAnalyzer() t.Run("Multiple SPF results - only first is parsed", func(t *testing.T) { header := "mail.example.com; spf=pass smtp.mailfrom=first@example.com; spf=fail smtp.mailfrom=second@example.com" diff --git a/pkg/analyzer/authentication_x_aligned_from.go b/pkg/analyzer/authentication_x_aligned_from.go index eb0cf98..36da2b0 100644 --- a/pkg/analyzer/authentication_x_aligned_from.go +++ b/pkg/analyzer/authentication_x_aligned_from.go @@ -61,5 +61,5 @@ func (a *AuthenticationAnalyzer) calculateXAlignedFromScore(results *api.Authent } } - return 100 + return 0 } diff --git a/pkg/analyzer/authentication_x_aligned_from_test.go b/pkg/analyzer/authentication_x_aligned_from_test.go index 0fdd69d..220ac39 100644 --- a/pkg/analyzer/authentication_x_aligned_from_test.go +++ b/pkg/analyzer/authentication_x_aligned_from_test.go @@ -66,7 +66,7 @@ func TestParseXAlignedFromResult(t *testing.T) { }, } - analyzer := NewAuthenticationAnalyzer("") + analyzer := NewAuthenticationAnalyzer() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { @@ -126,7 +126,7 @@ func TestCalculateXAlignedFromScore(t *testing.T) { }, } - analyzer := NewAuthenticationAnalyzer("") + analyzer := NewAuthenticationAnalyzer() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/analyzer/authentication_x_google_dkim_test.go b/pkg/analyzer/authentication_x_google_dkim_test.go index f9704c0..be29a08 100644 --- a/pkg/analyzer/authentication_x_google_dkim_test.go +++ b/pkg/analyzer/authentication_x_google_dkim_test.go @@ -60,7 +60,7 @@ func TestParseXGoogleDKIMResult(t *testing.T) { }, } - analyzer := NewAuthenticationAnalyzer("") + analyzer := NewAuthenticationAnalyzer() for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/pkg/analyzer/content.go b/pkg/analyzer/content.go index d14d157..87c423f 100644 --- a/pkg/analyzer/content.go +++ b/pkg/analyzer/content.go @@ -27,7 +27,6 @@ import ( "net/http" "net/url" "regexp" - "slices" "strings" "time" "unicode" @@ -38,10 +37,8 @@ import ( // ContentAnalyzer analyzes email content (HTML, links, images) type ContentAnalyzer struct { - Timeout time.Duration - httpClient *http.Client - listUnsubscribeURLs []string // URLs from List-Unsubscribe header - hasOneClickUnsubscribe bool // True if List-Unsubscribe-Post: List-Unsubscribe=One-Click + Timeout time.Duration + httpClient *http.Client } // NewContentAnalyzer creates a new content analyzer with configurable timeout @@ -113,13 +110,6 @@ func (c *ContentAnalyzer) AnalyzeContent(email *EmailMessage) *ContentResults { results.IsMultipart = len(email.Parts) > 1 - // Parse List-Unsubscribe header URLs for use in link detection - c.listUnsubscribeURLs = email.GetListUnsubscribeURLs() - - // Check for one-click unsubscribe support - listUnsubscribePost := email.Header.Get("List-Unsubscribe-Post") - c.hasOneClickUnsubscribe = strings.EqualFold(strings.TrimSpace(listUnsubscribePost), "List-Unsubscribe=One-Click") - // Get HTML and text parts htmlParts := email.GetHTMLParts() textParts := email.GetTextParts() @@ -341,14 +331,9 @@ func (c *ContentAnalyzer) getAttr(n *html.Node, key string) string { // isUnsubscribeLink checks if a link is an unsubscribe link func (c *ContentAnalyzer) isUnsubscribeLink(href string, node *html.Node) bool { - // First check: does the href match a URL from the List-Unsubscribe header? - if slices.Contains(c.listUnsubscribeURLs, href) { - return true - } - // Check href for unsubscribe keywords lowerHref := strings.ToLower(href) - unsubKeywords := []string{"unsubscribe", "opt-out", "optout", "remove", "list-unsubscribe", "отписване", "desubscripció", "zrušit odběr", "dad-danysgrifio", "afmeld", "abmelden", "διαγραφή", "darse de baja", "poistu postituslistalta", "se désabonner", "ביטול רישום", "leiratkozás", "cancella iscrizione", "登録を取り消す", "구독 해지", "വരിക്കാരനല്ലാതാകുക", "uitschrijven", "meld av", "odsubskrybuj", "cancelar assinatura", "cancelar subscrição", "dezabonare", "отписаться", "avsluta prenumeration", "zrušiť odber", "odjava", "üyeliği sonlandır", "відписатися", "hủy đăng ký", "退订", "退訂"} + unsubKeywords := []string{"unsubscribe", "opt-out", "optout", "remove", "list-unsubscribe"} for _, keyword := range unsubKeywords { if strings.Contains(lowerHref, keyword) { return true @@ -454,8 +439,7 @@ func (c *ContentAnalyzer) hasDomainMisalignment(href, linkText string) bool { // Extract the actual destination domain/email based on scheme var actualDomain string - switch parsedURL.Scheme { - case "mailto": + if parsedURL.Scheme == "mailto" { // Extract email address from mailto: URL // Format can be: mailto:user@domain.com or mailto:user@domain.com?subject=... mailtoAddr := parsedURL.Opaque @@ -473,8 +457,7 @@ func (c *ContentAnalyzer) hasDomainMisalignment(href, linkText string) bool { } else { return false // Invalid mailto } - case "http": - case "https": + } else if parsedURL.Scheme == "http" || parsedURL.Scheme == "https" { // Check if URL has a host if parsedURL.Host == "" { return false @@ -486,7 +469,7 @@ func (c *ContentAnalyzer) hasDomainMisalignment(href, linkText string) bool { actualDomain = actualDomain[:idx] } actualDomain = strings.ToLower(actualDomain) - default: + } else { // Skip checks for other URL schemes (tel, etc.) return false } @@ -509,8 +492,10 @@ func (c *ContentAnalyzer) hasDomainMisalignment(href, linkText string) bool { "email us", "contact us", "send email", "get in touch", "reach out", "contact", "email", "write to us", } - if slices.Contains(genericTexts, linkText) { - return false + for _, generic := range genericTexts { + if linkText == generic { + return false + } } // Extract domain-like patterns from link text using regex @@ -577,8 +562,10 @@ func (c *ContentAnalyzer) isSuspiciousURL(urlStr string, parsedURL *url.URL) boo "bit.ly", "tinyurl.com", "goo.gl", "ow.ly", "t.co", "buff.ly", "is.gd", "bl.ink", "short.io", } - if slices.Contains(shorteners, strings.ToLower(parsedURL.Host)) { - return true + for _, shortener := range shorteners { + if strings.ToLower(parsedURL.Host) == shortener { + return true + } } // Check for excessive subdomains (possible obfuscation) @@ -640,7 +627,7 @@ func (c *ContentAnalyzer) extractTextFromHTML(htmlContent string) string { var extract func(*html.Node) extract = func(n *html.Node) { if n.Type == html.TextNode { - text.WriteString(" " + n.Data) + text.WriteString(n.Data) } // Skip script and style tags if n.Type == html.ElementNode && (n.Data == "script" || n.Data == "style") { @@ -652,7 +639,7 @@ func (c *ContentAnalyzer) extractTextFromHTML(htmlContent string) string { } extract(doc) - return strings.TrimSpace(text.String()) + return text.String() } // calculateTextPlainConsistency compares plain text and HTML versions @@ -672,47 +659,30 @@ func (c *ContentAnalyzer) calculateTextPlainConsistency(plainText, htmlText stri return 0.0 } - // Count common words by building sets - plainWordSet := make(map[string]int) - for _, word := range plainWords { - plainWordSet[word]++ - } - - htmlWordSet := make(map[string]int) - for _, word := range htmlWords { - htmlWordSet[word]++ - } - - // Count matches: for each unique word, count minimum occurrences in both texts + // Count common words commonWords := 0 - for word, plainCount := range plainWordSet { - if htmlCount, exists := htmlWordSet[word]; exists { - // Count the minimum occurrences between both texts - if plainCount < htmlCount { - commonWords += plainCount - } else { - commonWords += htmlCount - } + plainWordSet := make(map[string]bool) + for _, word := range plainWords { + plainWordSet[word] = true + } + + for _, word := range htmlWords { + if plainWordSet[word] { + commonWords++ } } - // Calculate ratio using total words from both texts (union approach) - // This provides a balanced measure: perfect match = 1.0, partial overlap = 0.3-0.8 - totalWords := len(plainWords) + len(htmlWords) - if totalWords == 0 { + // Calculate ratio (Jaccard similarity approximation) + maxWords := len(plainWords) + if len(htmlWords) > maxWords { + maxWords = len(htmlWords) + } + + if maxWords == 0 { return 0.0 } - // Divide by average word count for better scoring - avgWords := float32(totalWords) / 2.0 - ratio := float32(commonWords) / avgWords - - // Cap at 1.0 for perfect matches - if ratio > 1.0 { - ratio = 1.0 - } - - return ratio + return float32(commonWords) / float32(maxWords) } // normalizeText normalizes text for comparison @@ -737,7 +707,6 @@ func (c *ContentAnalyzer) GenerateContentAnalysis(results *ContentResults) *api. HasHtml: api.PtrTo(results.HTMLContent != ""), HasPlaintext: api.PtrTo(results.TextContent != ""), HasUnsubscribeLink: api.PtrTo(results.HasUnsubscribe), - UnsubscribeMethods: &[]api.ContentAnalysisUnsubscribeMethods{}, } // Calculate text-to-image ratio (inverse of image-to-text) @@ -884,19 +853,8 @@ func (c *ContentAnalyzer) GenerateContentAnalysis(results *ContentResults) *api. // Unsubscribe methods if results.HasUnsubscribe { - *analysis.UnsubscribeMethods = append(*analysis.UnsubscribeMethods, api.Link) - } - - for _, url := range c.listUnsubscribeURLs { - if strings.HasPrefix(url, "mailto:") { - *analysis.UnsubscribeMethods = append(*analysis.UnsubscribeMethods, api.Mailto) - } else if strings.HasPrefix(url, "http:") || strings.HasPrefix(url, "https:") { - *analysis.UnsubscribeMethods = append(*analysis.UnsubscribeMethods, api.ListUnsubscribeHeader) - } - } - - if slices.Contains(*analysis.UnsubscribeMethods, api.ListUnsubscribeHeader) && c.hasOneClickUnsubscribe { - *analysis.UnsubscribeMethods = append(*analysis.UnsubscribeMethods, api.OneClick) + methods := []api.ContentAnalysisUnsubscribeMethods{api.Link} + analysis.UnsubscribeMethods = &methods } return analysis diff --git a/pkg/analyzer/content_test.go b/pkg/analyzer/content_test.go index 4ad01a8..0aa7ff9 100644 --- a/pkg/analyzer/content_test.go +++ b/pkg/analyzer/content_test.go @@ -76,17 +76,17 @@ func TestExtractTextFromHTML(t *testing.T) { { name: "Multiple elements", html: "

Title

Paragraph

", - expectedText: "Title Paragraph", + expectedText: "TitleParagraph", }, { name: "With script tag", html: "

Text

More

", - expectedText: "Text More", + expectedText: "TextMore", }, { name: "With style tag", html: "

Text

More

", - expectedText: "Text More", + expectedText: "TextMore", }, { name: "Empty HTML", @@ -144,74 +144,6 @@ func TestIsUnsubscribeLink(t *testing.T) { linkText: "Read more", expected: false, }, - // Multilingual keyword detection - URL path - { - name: "German abmelden in URL", - href: "https://example.com/abmelden?id=42", - linkText: "Click here", - expected: true, - }, - { - name: "French se-desabonner slug in URL (no accent/space - not detected by keyword)", - href: "https://example.com/se-desabonner?id=42", - linkText: "Click here", - expected: false, - }, - // Multilingual keyword detection - link text - { - name: "German Abmelden in link text", - href: "https://example.com/manage?id=42&lang=de", - linkText: "Abmelden", - expected: true, - }, - { - name: "French Se désabonner in link text", - href: "https://example.com/manage?id=42&lang=fr", - linkText: "Se désabonner", - expected: true, - }, - { - name: "Russian Отписаться in link text", - href: "https://example.com/manage?id=42&lang=ru", - linkText: "Отписаться", - expected: true, - }, - { - name: "Chinese 退订 in link text", - href: "https://example.com/manage?id=42&lang=zh", - linkText: "退订", - expected: true, - }, - { - name: "Japanese 登録を取り消す in link text", - href: "https://example.com/manage?id=42&lang=ja", - linkText: "登録を取り消す", - expected: true, - }, - { - name: "Korean 구독 해지 in link text", - href: "https://example.com/manage?id=42&lang=ko", - linkText: "구독 해지", - expected: true, - }, - { - name: "Dutch Uitschrijven in link text", - href: "https://example.com/manage?id=42&lang=nl", - linkText: "Uitschrijven", - expected: true, - }, - { - name: "Polish Odsubskrybuj in link text", - href: "https://example.com/manage?id=42&lang=pl", - linkText: "Odsubskrybuj", - expected: true, - }, - { - name: "Turkish Üyeliği sonlandır in link text", - href: "https://example.com/manage?id=42&lang=tr", - linkText: "Üyeliği sonlandır", - expected: true, - }, } analyzer := NewContentAnalyzer(5 * time.Second) diff --git a/pkg/analyzer/dns.go b/pkg/analyzer/dns.go index 29d8211..c76359c 100644 --- a/pkg/analyzer/dns.go +++ b/pkg/analyzer/dns.go @@ -22,6 +22,7 @@ package analyzer import ( + "net" "time" "git.happydns.org/happyDeliver/internal/api" @@ -30,31 +31,24 @@ import ( // DNSAnalyzer analyzes DNS records for email domains type DNSAnalyzer struct { Timeout time.Duration - resolver DNSResolver + resolver *net.Resolver } // NewDNSAnalyzer creates a new DNS analyzer with configurable timeout func NewDNSAnalyzer(timeout time.Duration) *DNSAnalyzer { - return NewDNSAnalyzerWithResolver(timeout, NewStandardDNSResolver()) -} - -// NewDNSAnalyzerWithResolver creates a new DNS analyzer with a custom resolver. -// If resolver is nil, a StandardDNSResolver will be used. -func NewDNSAnalyzerWithResolver(timeout time.Duration, resolver DNSResolver) *DNSAnalyzer { if timeout == 0 { timeout = 10 * time.Second // Default timeout } - if resolver == nil { - resolver = NewStandardDNSResolver() - } return &DNSAnalyzer{ - Timeout: timeout, - resolver: resolver, + Timeout: timeout, + resolver: &net.Resolver{ + PreferGo: true, + }, } } // AnalyzeDNS performs DNS validation for the email's domain -func (d *DNSAnalyzer) AnalyzeDNS(email *EmailMessage, headersResults *api.HeaderAnalysis) *api.DNSResults { +func (d *DNSAnalyzer) AnalyzeDNS(email *EmailMessage, authResults *api.AuthenticationResults, headersResults *api.HeaderAnalysis) *api.DNSResults { // Extract domain from From address if headersResults.DomainAlignment.FromDomain == nil || *headersResults.DomainAlignment.FromDomain == "" { return &api.DNSResults{ @@ -104,14 +98,19 @@ func (d *DNSAnalyzer) AnalyzeDNS(email *EmailMessage, headersResults *api.Header // SPF validates the MAIL FROM command, which corresponds to Return-Path results.SpfRecords = d.checkSPFRecords(spfDomain) - // Check DKIM records by parsing DKIM-Signature headers directly - for _, sig := range parseDKIMSignatures(email.Header["Dkim-Signature"]) { - dkimRecord := d.checkDKIMRecord(sig.Domain, sig.Selector) - if dkimRecord != nil { - if results.DkimRecords == nil { - results.DkimRecords = new([]api.DKIMRecord) + // Check DKIM records (from authentication results) + // DKIM can be for any domain, but typically the From domain + if authResults != nil && authResults.Dkim != nil { + for _, dkim := range *authResults.Dkim { + if dkim.Domain != nil && dkim.Selector != nil { + dkimRecord := d.checkDKIMRecord(*dkim.Domain, *dkim.Selector) + if dkimRecord != nil { + if results.DkimRecords == nil { + results.DkimRecords = new([]api.DKIMRecord) + } + *results.DkimRecords = append(*results.DkimRecords, *dkimRecord) + } } - *results.DkimRecords = append(*results.DkimRecords, *dkimRecord) } } @@ -125,70 +124,6 @@ func (d *DNSAnalyzer) AnalyzeDNS(email *EmailMessage, headersResults *api.Header return results } -// AnalyzeDomainOnly performs DNS validation for a domain without email context -// This is useful for checking domain configuration without sending an actual email -func (d *DNSAnalyzer) AnalyzeDomainOnly(domain string) *api.DNSResults { - results := &api.DNSResults{ - FromDomain: domain, - } - - // Check MX records - results.FromMxRecords = d.checkMXRecords(domain) - - // Check SPF records - results.SpfRecords = d.checkSPFRecords(domain) - - // Check DMARC record - results.DmarcRecord = d.checkDMARCRecord(domain) - - // Check BIMI record with default selector - results.BimiRecord = d.checkBIMIRecord(domain, "default") - - return results -} - -// CalculateDomainOnlyScore calculates the DNS score for domain-only tests -// Returns a score from 0-100 where higher is better -// This version excludes PTR and DKIM checks since they require email context -func (d *DNSAnalyzer) CalculateDomainOnlyScore(results *api.DNSResults) (int, string) { - if results == nil { - return 0, "" - } - - score := 0 - - // MX Records: 30 points (only one domain to check) - mxScore := d.calculateMXScore(results) - // Since calculateMXScore checks both From and RP domains, - // and we only have From domain, we use the full score - score += 30 * mxScore / 100 - - // SPF Records: 30 points - score += 30 * d.calculateSPFScore(results) / 100 - - // DMARC Record: 40 points - score += 40 * d.calculateDMARCScore(results) / 100 - - // BIMI Record: only bonus - if results.BimiRecord != nil && results.BimiRecord.Valid { - if score >= 100 { - return 100, "A+" - } - } - - // Ensure score doesn't exceed maximum - if score > 100 { - score = 100 - } - - // Ensure score is non-negative - if score < 0 { - score = 0 - } - - return score, ScoreToGradeKind(score) -} - // CalculateDNSScore calculates the DNS score from records results // Returns a score from 0-100 where higher is better // senderIP is the original sender IP address used for FCrDNS verification diff --git a/pkg/analyzer/dns_dkim.go b/pkg/analyzer/dns_dkim.go index 1a8a199..7ac858d 100644 --- a/pkg/analyzer/dns_dkim.go +++ b/pkg/analyzer/dns_dkim.go @@ -29,38 +29,6 @@ import ( "git.happydns.org/happyDeliver/internal/api" ) -// DKIMHeader holds the domain and selector extracted from a DKIM-Signature header. -type DKIMHeader struct { - Domain string - Selector string -} - -// parseDKIMSignatures extracts domain and selector from DKIM-Signature header values. -func parseDKIMSignatures(signatures []string) []DKIMHeader { - var results []DKIMHeader - for _, sig := range signatures { - var domain, selector string - for _, part := range strings.Split(sig, ";") { - kv := strings.SplitN(strings.TrimSpace(part), "=", 2) - if len(kv) != 2 { - continue - } - key := strings.TrimSpace(kv[0]) - val := strings.TrimSpace(kv[1]) - switch key { - case "d": - domain = val - case "s": - selector = val - } - } - if domain != "" && selector != "" { - results = append(results, DKIMHeader{Domain: domain, Selector: selector}) - } - } - return results -} - // checkapi.DKIMRecord looks up and validates DKIM record for a domain and selector func (d *DNSAnalyzer) checkDKIMRecord(domain, selector string) *api.DKIMRecord { // DKIM records are at: selector._domainkey.domain diff --git a/pkg/analyzer/dns_dkim_test.go b/pkg/analyzer/dns_dkim_test.go index 45da53c..8d94d20 100644 --- a/pkg/analyzer/dns_dkim_test.go +++ b/pkg/analyzer/dns_dkim_test.go @@ -26,220 +26,6 @@ import ( "time" ) -func TestParseDKIMSignatures(t *testing.T) { - tests := []struct { - name string - signatures []string - expected []DKIMHeader - }{ - { - name: "Empty input", - signatures: nil, - expected: nil, - }, - { - name: "Empty string", - signatures: []string{""}, - expected: nil, - }, - { - name: "Simple Gmail-style", - signatures: []string{ - `v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20210112; h=from:to:subject:date:message-id; bh=abcdef1234567890=; b=SIGNATURE_DATA_HERE==`, - }, - expected: []DKIMHeader{{Domain: "gmail.com", Selector: "20210112"}}, - }, - { - name: "Microsoft 365 style", - signatures: []string{ - `v=1; a=rsa-sha256; c=relaxed/relaxed; d=contoso.com; s=selector1; h=From:Date:Subject:Message-ID; bh=UErATeHehIIPIXPeUA==; b=SIGNATURE_DATA==`, - }, - expected: []DKIMHeader{{Domain: "contoso.com", Selector: "selector1"}}, - }, - { - name: "Tab-folded multiline (Postfix-style)", - signatures: []string{ - "v=1; a=rsa-sha256; c=relaxed/simple; d=nemunai.re; s=thot;\r\n\tt=1760866834; bh=YNB7c8Qgm8YGn9X1FAXTcdpO7t4YSZFiMrmpCfD/3zw=;\r\n\th=From:To:Subject;\r\n\tb=T4TFaypMpsHGYCl3PGLwmzOYRF11rYjC7lF8V5VFU+ldvG8WBpFn==", - }, - expected: []DKIMHeader{{Domain: "nemunai.re", Selector: "thot"}}, - }, - { - name: "Space-folded multiline (RFC-style)", - signatures: []string{ - "v=1; a=rsa-sha256; c=relaxed/relaxed;\r\n d=football.example.com; i=@football.example.com;\r\n q=dns/txt; s=test; t=1528637909; h=from:to:subject;\r\n bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;\r\n b=F45dVWDfMbQDGHJFlXUNB2HKfbCeLRyhDXgFpEL8Gwps==", - }, - expected: []DKIMHeader{{Domain: "football.example.com", Selector: "test"}}, - }, - { - name: "d= and s= on separate continuation lines", - signatures: []string{ - "v=1; a=rsa-sha256;\r\n\tc=relaxed/relaxed;\r\n\td=mycompany.com;\r\n\ts=selector1;\r\n\tbh=hash=;\r\n\tb=sig==", - }, - expected: []DKIMHeader{{Domain: "mycompany.com", Selector: "selector1"}}, - }, - { - name: "No space after semicolons", - signatures: []string{ - `v=1;a=rsa-sha256;c=relaxed/relaxed;d=example.net;s=mail;h=from:to:subject;bh=abc=;b=xyz==`, - }, - expected: []DKIMHeader{{Domain: "example.net", Selector: "mail"}}, - }, - { - name: "Multiple spaces after semicolons", - signatures: []string{ - `v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=myselector; bh=hash=; b=sig==`, - }, - expected: []DKIMHeader{{Domain: "example.com", Selector: "myselector"}}, - }, - { - name: "Ed25519 signature (RFC 8463)", - signatures: []string{ - "v=1; a=ed25519-sha256; c=relaxed/relaxed;\r\n d=football.example.com; i=@football.example.com;\r\n q=dns/txt; s=brisbane; t=1528637909; h=from:to:subject;\r\n bh=2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=;\r\n b=/gCrinpcQOoIfuHNQIbq4pgh9kyIK3AQ==", - }, - expected: []DKIMHeader{{Domain: "football.example.com", Selector: "brisbane"}}, - }, - { - name: "Multiple signatures (ESP double-signing)", - signatures: []string{ - `v=1; a=rsa-sha256; c=relaxed/relaxed; d=mydomain.com; s=mail; h=from:to:subject; bh=hash1=; b=sig1==`, - `v=1; a=rsa-sha256; c=relaxed/relaxed; d=sendib.com; s=mail; h=from:to:subject; bh=hash1=; b=sig2==`, - }, - expected: []DKIMHeader{ - {Domain: "mydomain.com", Selector: "mail"}, - {Domain: "sendib.com", Selector: "mail"}, - }, - }, - { - name: "Dual-algorithm signing (Ed25519 + RSA, same domain, different selectors)", - signatures: []string{ - `v=1; a=ed25519-sha256; c=relaxed/relaxed; d=football.example.com; s=brisbane; h=from:to:subject; bh=hash=; b=edSig==`, - `v=1; a=rsa-sha256; c=relaxed/relaxed; d=football.example.com; s=test; h=from:to:subject; bh=hash=; b=rsaSig==`, - }, - expected: []DKIMHeader{ - {Domain: "football.example.com", Selector: "brisbane"}, - {Domain: "football.example.com", Selector: "test"}, - }, - }, - { - name: "Amazon SES long selectors", - signatures: []string{ - `v=1; a=rsa-sha256; c=relaxed/simple; d=amazonses.com; s=224i4yxa5dv7c2xz3womw6peuabd; h=from:to:subject; bh=sesHash=; b=sesSig==`, - `v=1; a=rsa-sha256; c=relaxed/simple; d=customerdomain.io; s=ug7nbtf4gccmlpwj322ax3p6ow6fovbt; h=from:to:subject; bh=sesHash=; b=customSig==`, - }, - expected: []DKIMHeader{ - {Domain: "amazonses.com", Selector: "224i4yxa5dv7c2xz3womw6peuabd"}, - {Domain: "customerdomain.io", Selector: "ug7nbtf4gccmlpwj322ax3p6ow6fovbt"}, - }, - }, - { - name: "Subdomain in d=", - signatures: []string{ - `v=1; a=rsa-sha256; c=relaxed/relaxed; d=mail.example.co.uk; s=dkim2025; h=from:to:subject; bh=hash=; b=sig==`, - }, - expected: []DKIMHeader{{Domain: "mail.example.co.uk", Selector: "dkim2025"}}, - }, - { - name: "Deeply nested subdomain", - signatures: []string{ - `v=1; a=rsa-sha256; c=relaxed/relaxed; d=bounce.transactional.mail.example.com; s=s2048; h=from:to:subject; bh=hash=; b=sig==`, - }, - expected: []DKIMHeader{{Domain: "bounce.transactional.mail.example.com", Selector: "s2048"}}, - }, - { - name: "Selector with hyphens (Microsoft 365 custom domain style)", - signatures: []string{ - `v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=selector1-contoso-com; h=from:to:subject; bh=hash=; b=sig==`, - }, - expected: []DKIMHeader{{Domain: "example.com", Selector: "selector1-contoso-com"}}, - }, - { - name: "Selector with dots", - signatures: []string{ - `v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; s=smtp.mail; h=from:to:subject; bh=hash=; b=sig==`, - }, - expected: []DKIMHeader{{Domain: "example.com", Selector: "smtp.mail"}}, - }, - { - name: "Single-character selector", - signatures: []string{ - `v=1; a=rsa-sha256; c=relaxed/relaxed; d=tiny.io; s=x; h=from:to:subject; bh=hash=; b=sig==`, - }, - expected: []DKIMHeader{{Domain: "tiny.io", Selector: "x"}}, - }, - { - name: "Postmark-style timestamp selector, s= before d=", - signatures: []string{ - `v=1; a=rsa-sha1; c=relaxed/relaxed; s=20130519032151pm; d=postmarkapp.com; h=From:Date:Subject; bh=vYFvy46eesUDGJ45hyBTH30JfN4=; b=iHeFQ+7rCiSQs3DPjR2eUSZSv4i==`, - }, - expected: []DKIMHeader{{Domain: "postmarkapp.com", Selector: "20130519032151pm"}}, - }, - { - name: "d= and s= at the very end", - signatures: []string{ - `v=1; a=rsa-sha256; c=relaxed/relaxed; h=from:to:subject; bh=hash=; b=sig==; d=example.net; s=trailing`, - }, - expected: []DKIMHeader{{Domain: "example.net", Selector: "trailing"}}, - }, - { - name: "Full tag set", - signatures: []string{ - `v=1; a=rsa-sha256; d=example.com; s=selector1; c=relaxed/simple; q=dns/txt; i=user@example.com; t=1255993973; x=1256598773; h=From:Sender:Reply-To:Subject:Date:Message-Id:To:Cc; bh=+7qxGePcmmrtZAIVQAtkSSGHfQ/ftNuvUTWJ3vXC9Zc=; b=dB85+qM+If1KGQmqMLNpqLgNtUaG5dhGjYjQD6/QXtXmViJx8tf9gLEjcHr+musLCAvr0Fsn1DA3ZLLlUxpf4AR==`, - }, - expected: []DKIMHeader{{Domain: "example.com", Selector: "selector1"}}, - }, - { - name: "Missing d= tag", - signatures: []string{ - `v=1; a=rsa-sha256; c=relaxed/relaxed; s=selector1; h=from:to; bh=hash=; b=sig==`, - }, - expected: nil, - }, - { - name: "Missing s= tag", - signatures: []string{ - `v=1; a=rsa-sha256; c=relaxed/relaxed; d=example.com; h=from:to; bh=hash=; b=sig==`, - }, - expected: nil, - }, - { - name: "Missing both d= and s= tags", - signatures: []string{ - `v=1; a=rsa-sha256; c=relaxed/relaxed; h=from:to; bh=hash=; b=sig==`, - }, - expected: nil, - }, - { - name: "Mix of valid and invalid signatures", - signatures: []string{ - `v=1; a=rsa-sha256; c=relaxed/relaxed; d=good.com; s=sel1; h=from:to; bh=hash=; b=sig==`, - `v=1; a=rsa-sha256; c=relaxed/relaxed; s=orphan; h=from:to; bh=hash=; b=sig==`, - `v=1; a=rsa-sha256; c=relaxed/relaxed; d=also-good.com; s=sel2; h=from:to; bh=hash=; b=sig==`, - }, - expected: []DKIMHeader{ - {Domain: "good.com", Selector: "sel1"}, - {Domain: "also-good.com", Selector: "sel2"}, - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := parseDKIMSignatures(tt.signatures) - if len(result) != len(tt.expected) { - t.Fatalf("parseDKIMSignatures() returned %d results, want %d\n got: %+v\n want: %+v", len(result), len(tt.expected), result, tt.expected) - } - for i := range tt.expected { - if result[i].Domain != tt.expected[i].Domain { - t.Errorf("result[%d].Domain = %q, want %q", i, result[i].Domain, tt.expected[i].Domain) - } - if result[i].Selector != tt.expected[i].Selector { - t.Errorf("result[%d].Selector = %q, want %q", i, result[i].Selector, tt.expected[i].Selector) - } - } - }) - } -} - func TestValidateDKIM(t *testing.T) { tests := []struct { name string diff --git a/pkg/analyzer/dns_resolver.go b/pkg/analyzer/dns_resolver.go deleted file mode 100644 index f60484f..0000000 --- a/pkg/analyzer/dns_resolver.go +++ /dev/null @@ -1,80 +0,0 @@ -// This file is part of the happyDeliver (R) project. -// Copyright (c) 2025 happyDomain -// Authors: Pierre-Olivier Mercier, et al. -// -// This program is offered under a commercial and under the AGPL license. -// For commercial licensing, contact us at . -// -// For AGPL licensing: -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package analyzer - -import ( - "context" - "net" -) - -// DNSResolver defines the interface for DNS resolution operations. -// This interface abstracts DNS lookups to allow for custom implementations, -// such as mock resolvers for testing or caching resolvers for performance. -type DNSResolver interface { - // LookupMX returns the DNS MX records for the given domain. - LookupMX(ctx context.Context, name string) ([]*net.MX, error) - - // LookupTXT returns the DNS TXT records for the given domain. - LookupTXT(ctx context.Context, name string) ([]string, error) - - // LookupAddr performs a reverse lookup for the given IP address, - // returning a list of hostnames mapping to that address. - LookupAddr(ctx context.Context, addr string) ([]string, error) - - // LookupHost looks up the given hostname using the local resolver. - // It returns a slice of that host's addresses (IPv4 and IPv6). - LookupHost(ctx context.Context, host string) ([]string, error) -} - -// StandardDNSResolver is the default DNS resolver implementation that uses net.Resolver. -type StandardDNSResolver struct { - resolver *net.Resolver -} - -// NewStandardDNSResolver creates a new StandardDNSResolver with default settings. -func NewStandardDNSResolver() DNSResolver { - return &StandardDNSResolver{ - resolver: &net.Resolver{ - PreferGo: true, - }, - } -} - -// LookupMX implements DNSResolver.LookupMX using net.Resolver. -func (r *StandardDNSResolver) LookupMX(ctx context.Context, name string) ([]*net.MX, error) { - return r.resolver.LookupMX(ctx, name) -} - -// LookupTXT implements DNSResolver.LookupTXT using net.Resolver. -func (r *StandardDNSResolver) LookupTXT(ctx context.Context, name string) ([]string, error) { - return r.resolver.LookupTXT(ctx, name) -} - -// LookupAddr implements DNSResolver.LookupAddr using net.Resolver. -func (r *StandardDNSResolver) LookupAddr(ctx context.Context, addr string) ([]string, error) { - return r.resolver.LookupAddr(ctx, addr) -} - -// LookupHost implements DNSResolver.LookupHost using net.Resolver. -func (r *StandardDNSResolver) LookupHost(ctx context.Context, host string) ([]string, error) { - return r.resolver.LookupHost(ctx, host) -} diff --git a/pkg/analyzer/dns_spf.go b/pkg/analyzer/dns_spf.go index bfa1640..fa819c1 100644 --- a/pkg/analyzer/dns_spf.go +++ b/pkg/analyzer/dns_spf.go @@ -33,12 +33,11 @@ import ( // checkSPFRecords looks up and validates SPF records for a domain, including resolving include: directives func (d *DNSAnalyzer) checkSPFRecords(domain string) *[]api.SPFRecord { visited := make(map[string]bool) - return d.resolveSPFRecords(domain, visited, 0, true) + return d.resolveSPFRecords(domain, visited, 0) } // resolveSPFRecords recursively resolves SPF records including include: directives -// isMainRecord indicates if this is the primary domain's record (not an included one) -func (d *DNSAnalyzer) resolveSPFRecords(domain string, visited map[string]bool, depth int, isMainRecord bool) *[]api.SPFRecord { +func (d *DNSAnalyzer) resolveSPFRecords(domain string, visited map[string]bool, depth int) *[]api.SPFRecord { const maxDepth = 10 // Prevent infinite recursion if depth > maxDepth { @@ -104,7 +103,7 @@ func (d *DNSAnalyzer) resolveSPFRecords(domain string, visited map[string]bool, } // Basic validation - validationErr := d.validateSPF(spfRecord, isMainRecord) + validationErr := d.validateSPF(spfRecord) // Extract the "all" mechanism qualifier var allQualifier *api.SPFRecordAllQualifier @@ -141,7 +140,7 @@ func (d *DNSAnalyzer) resolveSPFRecords(domain string, visited map[string]bool, if redirectDomain != "" { // redirect= replaces the current domain's policy entirely // Only follow if no other mechanisms matched (per RFC 7208) - redirectRecords := d.resolveSPFRecords(redirectDomain, visited, depth+1, false) + redirectRecords := d.resolveSPFRecords(redirectDomain, visited, depth+1) if redirectRecords != nil { results = append(results, *redirectRecords...) } @@ -151,7 +150,7 @@ func (d *DNSAnalyzer) resolveSPFRecords(domain string, visited map[string]bool, // Extract and resolve include: directives includes := d.extractSPFIncludes(spfRecord) for _, includeDomain := range includes { - includedRecords := d.resolveSPFRecords(includeDomain, visited, depth+1, false) + includedRecords := d.resolveSPFRecords(includeDomain, visited, depth+1) if includedRecords != nil { results = append(results, *includedRecords...) } @@ -191,12 +190,8 @@ func (d *DNSAnalyzer) isValidSPFMechanism(token string) error { // Check if it's a modifier (contains =) if strings.Contains(mechanism, "=") { - // Allow known modifiers: redirect=, exp=, and RFC 6652 modifiers (ra=, rp=, rr=) - if strings.HasPrefix(mechanism, "redirect=") || - strings.HasPrefix(mechanism, "exp=") || - strings.HasPrefix(mechanism, "ra=") || - strings.HasPrefix(mechanism, "rp=") || - strings.HasPrefix(mechanism, "rr=") { + // Only allow known modifiers: redirect= and exp= + if strings.HasPrefix(mechanism, "redirect=") || strings.HasPrefix(mechanism, "exp=") { return nil } @@ -241,8 +236,7 @@ func (d *DNSAnalyzer) isValidSPFMechanism(token string) error { } // validateSPF performs basic SPF record validation -// isMainRecord indicates if this is the primary domain's record (not an included one) -func (d *DNSAnalyzer) validateSPF(record string, isMainRecord bool) error { +func (d *DNSAnalyzer) validateSPF(record string) error { // Must start with v=spf1 if !strings.HasPrefix(record, "v=spf1") { return fmt.Errorf("SPF record must start with 'v=spf1'") @@ -275,22 +269,19 @@ func (d *DNSAnalyzer) validateSPF(record string, isMainRecord bool) error { return nil } - // Only check for 'all' mechanism on the main record, not on included records - if isMainRecord { - // Check for common syntax issues - // Should have a final mechanism (all, +all, -all, ~all, ?all) - validEndings := []string{" all", " +all", " -all", " ~all", " ?all"} - hasValidEnding := false - for _, ending := range validEndings { - if strings.HasSuffix(record, ending) { - hasValidEnding = true - break - } + // Check for common syntax issues + // Should have a final mechanism (all, +all, -all, ~all, ?all) + validEndings := []string{" all", " +all", " -all", " ~all", " ?all"} + hasValidEnding := false + for _, ending := range validEndings { + if strings.HasSuffix(record, ending) { + hasValidEnding = true + break } + } - if !hasValidEnding { - return fmt.Errorf("SPF record should end with an 'all' mechanism (e.g., '-all', '~all') or have a 'redirect=' modifier") - } + if !hasValidEnding { + return fmt.Errorf("SPF record should end with an 'all' mechanism (e.g., '-all', '~all') or have a 'redirect=' modifier") } return nil diff --git a/pkg/analyzer/dns_spf_test.go b/pkg/analyzer/dns_spf_test.go index 2e794ce..bc51a6f 100644 --- a/pkg/analyzer/dns_spf_test.go +++ b/pkg/analyzer/dns_spf_test.go @@ -122,39 +122,13 @@ func TestValidateSPF(t *testing.T) { expectError: true, errorMsg: "unknown modifier", }, - { - name: "Valid SPF with RFC 6652 ra modifier", - record: "v=spf1 mx ra=postmaster -all", - expectError: false, - }, - { - name: "Valid SPF with RFC 6652 rp modifier", - record: "v=spf1 mx rp=100 -all", - expectError: false, - }, - { - name: "Valid SPF with RFC 6652 rr modifier", - record: "v=spf1 mx rr=all -all", - expectError: false, - }, - { - name: "Valid SPF with all RFC 6652 modifiers", - record: "v=spf1 mx ra=postmaster rp=50 rr=fail -all", - expectError: false, - }, - { - name: "Valid SPF with RFC 6652 modifiers and redirect", - record: "v=spf1 ip4:192.0.2.0/24 ra=abuse redirect=_spf.example.com", - expectError: false, - }, } analyzer := NewDNSAnalyzer(5 * time.Second) for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Test as main record (isMainRecord = true) since these tests check overall SPF validity - err := analyzer.validateSPF(tt.record, true) + err := analyzer.validateSPF(tt.record) if tt.expectError { if err == nil { t.Errorf("validateSPF(%q) expected error but got nil", tt.record) @@ -170,74 +144,6 @@ func TestValidateSPF(t *testing.T) { } } -func TestValidateSPF_IncludedRecords(t *testing.T) { - tests := []struct { - name string - record string - isMainRecord bool - expectError bool - errorMsg string - }{ - { - name: "Main record without 'all' - should error", - record: "v=spf1 include:_spf.example.com", - isMainRecord: true, - expectError: true, - errorMsg: "should end with an 'all' mechanism", - }, - { - name: "Included record without 'all' - should NOT error", - record: "v=spf1 include:_spf.example.com", - isMainRecord: false, - expectError: false, - }, - { - name: "Included record with only mechanisms - should NOT error", - record: "v=spf1 ip4:192.0.2.0/24 mx", - isMainRecord: false, - expectError: false, - }, - { - name: "Main record with only mechanisms - should error", - record: "v=spf1 ip4:192.0.2.0/24 mx", - isMainRecord: true, - expectError: true, - errorMsg: "should end with an 'all' mechanism", - }, - { - name: "Included record with 'all' - valid", - record: "v=spf1 ip4:192.0.2.0/24 -all", - isMainRecord: false, - expectError: false, - }, - { - name: "Main record with 'all' - valid", - record: "v=spf1 ip4:192.0.2.0/24 -all", - isMainRecord: true, - expectError: false, - }, - } - - analyzer := NewDNSAnalyzer(5 * time.Second) - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := analyzer.validateSPF(tt.record, tt.isMainRecord) - if tt.expectError { - if err == nil { - t.Errorf("validateSPF(%q, isMainRecord=%v) expected error but got nil", tt.record, tt.isMainRecord) - } else if tt.errorMsg != "" && !strings.Contains(err.Error(), tt.errorMsg) { - t.Errorf("validateSPF(%q, isMainRecord=%v) error = %q, want error containing %q", tt.record, tt.isMainRecord, err.Error(), tt.errorMsg) - } - } else { - if err != nil { - t.Errorf("validateSPF(%q, isMainRecord=%v) unexpected error: %v", tt.record, tt.isMainRecord, err) - } - } - }) - } -} - func TestExtractSPFRedirect(t *testing.T) { tests := []struct { name string diff --git a/pkg/analyzer/headers.go b/pkg/analyzer/headers.go index 37718bb..7e65571 100644 --- a/pkg/analyzer/headers.go +++ b/pkg/analyzer/headers.go @@ -52,14 +52,13 @@ func (h *HeaderAnalyzer) CalculateHeaderScore(analysis *api.HeaderAnalysis) (int maxGrade := 6 headers := *analysis.Headers - // RP and From alignment (25 points) - if analysis.DomainAlignment.Aligned == nil || !*analysis.DomainAlignment.RelaxedAligned { - // Bad domain alignment, cap grade to C - maxGrade -= 2 - } else if *analysis.DomainAlignment.Aligned { - score += 25 - } else if *analysis.DomainAlignment.RelaxedAligned { + // RP and From alignment (20 points) + if analysis.DomainAlignment.Aligned != nil && *analysis.DomainAlignment.Aligned { score += 20 + } else if analysis.DomainAlignment.RelaxedAligned != nil && *analysis.DomainAlignment.RelaxedAligned { + score += 15 + } else { + maxGrade -= 2 } // Check required headers (RFC 5322) - 30 points @@ -80,7 +79,7 @@ func (h *HeaderAnalyzer) CalculateHeaderScore(analysis *api.HeaderAnalysis) (int maxGrade = 1 } - // Check recommended headers (15 points) + // Check recommended headers (20 points) recommendedHeaders := []string{"subject", "to"} // Add reply-to when from is a no-reply address @@ -96,7 +95,7 @@ func (h *HeaderAnalyzer) CalculateHeaderScore(analysis *api.HeaderAnalysis) (int presentRecommended++ } } - score += presentRecommended * 15 / recommendedCount + score += presentRecommended * 20 / recommendedCount if presentRecommended < recommendedCount { maxGrade -= 1 @@ -109,13 +108,6 @@ func (h *HeaderAnalyzer) CalculateHeaderScore(analysis *api.HeaderAnalysis) (int maxGrade -= 1 } - // Check MIME-Version header (-5 points if present but not "1.0") - if check, exists := headers["mime-version"]; exists && check.Present { - if check.Valid != nil && !*check.Valid { - score -= 5 - } - } - // Check Message-ID format (10 points) if check, exists := headers["message-id"]; exists && check.Present { // If Valid is set and true, award points @@ -243,7 +235,7 @@ func (h *HeaderAnalyzer) formatAddress(addr *mail.Address) string { } // GenerateHeaderAnalysis creates structured header analysis from email -func (h *HeaderAnalyzer) GenerateHeaderAnalysis(email *EmailMessage, authResults *api.AuthenticationResults) *api.HeaderAnalysis { +func (h *HeaderAnalyzer) GenerateHeaderAnalysis(email *EmailMessage) *api.HeaderAnalysis { if email == nil { return nil } @@ -273,10 +265,6 @@ func (h *HeaderAnalyzer) GenerateHeaderAnalysis(email *EmailMessage, authResults headers[strings.ToLower(headerName)] = *check } - // Check MIME-Version header (recommended but absence is not penalized) - mimeVersionCheck := h.checkHeader(email, "MIME-Version", "recommended") - headers[strings.ToLower("MIME-Version")] = *mimeVersionCheck - // Check optional headers optionalHeaders := []string{"List-Unsubscribe", "List-Unsubscribe-Post"} for _, headerName := range optionalHeaders { @@ -293,7 +281,7 @@ func (h *HeaderAnalyzer) GenerateHeaderAnalysis(email *EmailMessage, authResults } // Domain alignment - domainAlignment := h.analyzeDomainAlignment(email, authResults) + domainAlignment := h.analyzeDomainAlignment(email) if domainAlignment != nil { analysis.DomainAlignment = domainAlignment } @@ -331,21 +319,12 @@ func (h *HeaderAnalyzer) checkHeader(email *EmailMessage, headerName string, imp valid = false headerIssues = append(headerIssues, "Invalid Message-ID format (should be )") } - if len(email.Header["Message-Id"]) > 1 { - valid = false - headerIssues = append(headerIssues, fmt.Sprintf("Multiple Message-ID headers found (%d); only one is allowed", len(email.Header["Message-Id"]))) - } case "Date": // Validate date format if _, err := h.parseEmailDate(value); err != nil { valid = false headerIssues = append(headerIssues, fmt.Sprintf("Invalid date format: %v", err)) } - case "MIME-Version": - if value != "1.0" { - valid = false - headerIssues = append(headerIssues, fmt.Sprintf("MIME-Version should be '1.0', got '%s'", value)) - } case "From", "To", "Cc", "Bcc", "Reply-To", "Sender", "Resent-From", "Resent-To", "Return-Path": // Parse address header using net/mail and get normalized address if normalizedAddr, err := h.validateAddressHeader(value); err != nil { @@ -373,8 +352,8 @@ func (h *HeaderAnalyzer) checkHeader(email *EmailMessage, headerName string, imp return check } -// analyzeDomainAlignment checks domain alignment between headers and DKIM signatures -func (h *HeaderAnalyzer) analyzeDomainAlignment(email *EmailMessage, authResults *api.AuthenticationResults) *api.DomainAlignment { +// analyzeDomainAlignment checks domain alignment between headers +func (h *HeaderAnalyzer) analyzeDomainAlignment(email *EmailMessage) *api.DomainAlignment { alignment := &api.DomainAlignment{ Aligned: api.PtrTo(true), RelaxedAligned: api.PtrTo(true), @@ -404,45 +383,14 @@ func (h *HeaderAnalyzer) analyzeDomainAlignment(email *EmailMessage, authResults } } - // Extract DKIM domains from authentication results - var dkimDomains []api.DKIMDomainInfo - if authResults != nil && authResults.Dkim != nil { - for _, dkim := range *authResults.Dkim { - if dkim.Domain != nil && *dkim.Domain != "" { - domain := *dkim.Domain - orgDomain := h.getOrganizationalDomain(domain) - dkimDomains = append(dkimDomains, api.DKIMDomainInfo{ - Domain: domain, - OrgDomain: orgDomain, - }) - } - } - } - if len(dkimDomains) > 0 { - alignment.DkimDomains = &dkimDomains - } - // Check alignment (strict and relaxed) issues := []string{} - - // hasReturnPath and hasDKIM track whether we have these fields to check - hasReturnPath := alignment.FromDomain != nil && alignment.ReturnPathDomain != nil - hasDKIM := alignment.FromDomain != nil && len(dkimDomains) > 0 - - // If neither Return-Path nor DKIM is present, keep default alignment (true) - // Otherwise, at least one must be aligned for overall alignment to be true - strictAligned := !hasReturnPath && !hasDKIM - relaxedAligned := !hasReturnPath && !hasDKIM - - // Check Return-Path alignment - rpStrictAligned := false - rpRelaxedAligned := false - if hasReturnPath { + if alignment.FromDomain != nil && alignment.ReturnPathDomain != nil { fromDomain := *alignment.FromDomain rpDomain := *alignment.ReturnPathDomain // Strict alignment: exact match (case-insensitive) - rpStrictAligned = strings.EqualFold(fromDomain, rpDomain) + strictAligned := strings.EqualFold(fromDomain, rpDomain) // Relaxed alignment: organizational domain match var fromOrgDomain, rpOrgDomain string @@ -452,67 +400,20 @@ func (h *HeaderAnalyzer) analyzeDomainAlignment(email *EmailMessage, authResults if alignment.ReturnPathOrgDomain != nil { rpOrgDomain = *alignment.ReturnPathOrgDomain } - rpRelaxedAligned = strings.EqualFold(fromOrgDomain, rpOrgDomain) + relaxedAligned := strings.EqualFold(fromOrgDomain, rpOrgDomain) - if !rpStrictAligned { - if rpRelaxedAligned { + *alignment.Aligned = strictAligned + *alignment.RelaxedAligned = relaxedAligned + + if !strictAligned { + if relaxedAligned { issues = append(issues, fmt.Sprintf("Return-Path domain (%s) does not exactly match From domain (%s), but satisfies relaxed alignment (organizational domain: %s)", rpDomain, fromDomain, fromOrgDomain)) } else { issues = append(issues, fmt.Sprintf("Return-Path domain (%s) does not match From domain (%s) - neither strict nor relaxed alignment", rpDomain, fromDomain)) } } - - strictAligned = rpStrictAligned - relaxedAligned = rpRelaxedAligned } - // Check DKIM alignment - dkimStrictAligned := false - dkimRelaxedAligned := false - if hasDKIM { - fromDomain := *alignment.FromDomain - var fromOrgDomain string - if alignment.FromOrgDomain != nil { - fromOrgDomain = *alignment.FromOrgDomain - } - - for _, dkimDomain := range dkimDomains { - // Check strict alignment for this DKIM signature - if strings.EqualFold(fromDomain, dkimDomain.Domain) { - dkimStrictAligned = true - } - - // Check relaxed alignment for this DKIM signature - if strings.EqualFold(fromOrgDomain, dkimDomain.OrgDomain) { - dkimRelaxedAligned = true - } - } - - if !dkimStrictAligned && !dkimRelaxedAligned { - // List all DKIM domains that failed alignment - dkimDomainsList := []string{} - for _, dkimDomain := range dkimDomains { - dkimDomainsList = append(dkimDomainsList, dkimDomain.Domain) - } - issues = append(issues, fmt.Sprintf("DKIM signature domains (%s) do not align with From domain (%s) - neither strict nor relaxed alignment", strings.Join(dkimDomainsList, ", "), fromDomain)) - } else if !dkimStrictAligned && dkimRelaxedAligned { - // DKIM has relaxed alignment but not strict - issues = append(issues, fmt.Sprintf("DKIM signature domains satisfy relaxed alignment with From domain (%s) but not strict alignment (organizational domain: %s)", fromDomain, fromOrgDomain)) - } - - // Overall alignment requires at least one method (Return-Path OR DKIM) to be aligned - // For DMARC compliance, at least one of SPF or DKIM must be aligned - if dkimStrictAligned { - strictAligned = true - } - if dkimRelaxedAligned { - relaxedAligned = true - } - } - - *alignment.Aligned = strictAligned - *alignment.RelaxedAligned = relaxedAligned - if len(issues) > 0 { alignment.Issues = &issues } diff --git a/pkg/analyzer/headers_test.go b/pkg/analyzer/headers_test.go index 2513e6f..7896a5c 100644 --- a/pkg/analyzer/headers_test.go +++ b/pkg/analyzer/headers_test.go @@ -24,7 +24,6 @@ package analyzer import ( "net/mail" "net/textproto" - "strings" "testing" "git.happydns.org/happyDeliver/internal/api" @@ -83,8 +82,8 @@ func TestCalculateHeaderScore(t *testing.T) { Date: "Mon, 01 Jan 2024 12:00:00 +0000", Parts: []MessagePart{{ContentType: "text/plain", Content: "test"}}, }, - minScore: 80, - maxScore: 90, + minScore: 40, + maxScore: 80, }, { name: "Invalid Message-ID format", @@ -111,7 +110,7 @@ func TestCalculateHeaderScore(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Generate header analysis first - analysis := analyzer.GenerateHeaderAnalysis(tt.email, nil) + analysis := analyzer.GenerateHeaderAnalysis(tt.email) score, _ := analyzer.CalculateHeaderScore(analysis) if score < tt.minScore || score > tt.maxScore { t.Errorf("CalculateHeaderScore() = %v, want between %v and %v", score, tt.minScore, tt.maxScore) @@ -361,7 +360,7 @@ func TestAnalyzeDomainAlignment(t *testing.T) { }), } - alignment := analyzer.analyzeDomainAlignment(email, nil) + alignment := analyzer.analyzeDomainAlignment(email) if alignment == nil { t.Fatal("Expected non-nil alignment") @@ -699,7 +698,7 @@ func TestGenerateHeaderAnalysis_WithReceivedChain(t *testing.T) { "from relay.example.com (relay.example.com [192.0.2.2]) by mail.example.com with SMTP id DEF456; Mon, 01 Jan 2024 11:59:00 +0000", } - analysis := analyzer.GenerateHeaderAnalysis(email, nil) + analysis := analyzer.GenerateHeaderAnalysis(email) if analysis == nil { t.Fatal("GenerateHeaderAnalysis returned nil") @@ -924,156 +923,3 @@ func equalStrPtr(a, b *string) bool { } return *a == *b } - -func TestAnalyzeDomainAlignment_WithDKIM(t *testing.T) { - tests := []struct { - name string - fromHeader string - returnPath string - dkimDomains []string - expectStrictAligned bool - expectRelaxedAligned bool - expectIssuesContain string - }{ - { - name: "DKIM strict alignment with From domain", - fromHeader: "sender@example.com", - returnPath: "", - dkimDomains: []string{"example.com"}, - expectStrictAligned: true, - expectRelaxedAligned: true, - expectIssuesContain: "", - }, - { - name: "DKIM relaxed alignment only", - fromHeader: "sender@mail.example.com", - returnPath: "", - dkimDomains: []string{"example.com"}, - expectStrictAligned: false, - expectRelaxedAligned: true, - expectIssuesContain: "relaxed alignment", - }, - { - name: "DKIM no alignment", - fromHeader: "sender@example.com", - returnPath: "", - dkimDomains: []string{"different.com"}, - expectStrictAligned: false, - expectRelaxedAligned: false, - expectIssuesContain: "do not align", - }, - { - name: "Multiple DKIM signatures - one aligns", - fromHeader: "sender@example.com", - returnPath: "", - dkimDomains: []string{"different.com", "example.com"}, - expectStrictAligned: true, - expectRelaxedAligned: true, - expectIssuesContain: "", - }, - { - name: "Return-Path misaligned but DKIM aligned", - fromHeader: "sender@example.com", - returnPath: "bounce@different.com", - dkimDomains: []string{"example.com"}, - expectStrictAligned: true, - expectRelaxedAligned: true, - expectIssuesContain: "Return-Path", - }, - { - name: "Return-Path aligned, no DKIM", - fromHeader: "sender@example.com", - returnPath: "bounce@example.com", - dkimDomains: []string{}, - expectStrictAligned: true, - expectRelaxedAligned: true, - expectIssuesContain: "", - }, - { - name: "Both Return-Path and DKIM misaligned", - fromHeader: "sender@example.com", - returnPath: "bounce@other.com", - dkimDomains: []string{"different.com"}, - expectStrictAligned: false, - expectRelaxedAligned: false, - expectIssuesContain: "do not", - }, - } - - analyzer := NewHeaderAnalyzer() - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - email := &EmailMessage{ - Header: createHeaderWithFields(map[string]string{ - "From": tt.fromHeader, - "Return-Path": tt.returnPath, - }), - } - - // Create authentication results with DKIM signatures - var authResults *api.AuthenticationResults - if len(tt.dkimDomains) > 0 { - dkimResults := make([]api.AuthResult, 0, len(tt.dkimDomains)) - for _, domain := range tt.dkimDomains { - dkimResults = append(dkimResults, api.AuthResult{ - Result: api.AuthResultResultPass, - Domain: &domain, - }) - } - authResults = &api.AuthenticationResults{ - Dkim: &dkimResults, - } - } - - alignment := analyzer.analyzeDomainAlignment(email, authResults) - - if alignment == nil { - t.Fatal("Expected non-nil alignment") - } - - if alignment.Aligned == nil { - t.Fatal("Expected non-nil Aligned field") - } - - if *alignment.Aligned != tt.expectStrictAligned { - t.Errorf("Aligned = %v, want %v", *alignment.Aligned, tt.expectStrictAligned) - } - - if alignment.RelaxedAligned == nil { - t.Fatal("Expected non-nil RelaxedAligned field") - } - - if *alignment.RelaxedAligned != tt.expectRelaxedAligned { - t.Errorf("RelaxedAligned = %v, want %v", *alignment.RelaxedAligned, tt.expectRelaxedAligned) - } - - // Check DKIM domains are populated - if len(tt.dkimDomains) > 0 { - if alignment.DkimDomains == nil { - t.Error("Expected DkimDomains to be populated") - } else if len(*alignment.DkimDomains) != len(tt.dkimDomains) { - t.Errorf("Expected %d DKIM domains, got %d", len(tt.dkimDomains), len(*alignment.DkimDomains)) - } - } - - // Check issues contain expected string - if tt.expectIssuesContain != "" { - if alignment.Issues == nil || len(*alignment.Issues) == 0 { - t.Errorf("Expected issues to contain '%s', but no issues found", tt.expectIssuesContain) - } else { - found := false - for _, issue := range *alignment.Issues { - if strings.Contains(strings.ToLower(issue), strings.ToLower(tt.expectIssuesContain)) { - found = true - break - } - } - if !found { - t.Errorf("Expected issues to contain '%s', but found: %v", tt.expectIssuesContain, *alignment.Issues) - } - } - } - }) - } -} diff --git a/pkg/analyzer/parser.go b/pkg/analyzer/parser.go index 00de151..ca3cb46 100644 --- a/pkg/analyzer/parser.go +++ b/pkg/analyzer/parser.go @@ -28,9 +28,16 @@ import ( "mime/multipart" "net/mail" "net/textproto" + "os" "strings" ) +var hostname = "" + +func init() { + hostname, _ = os.Hostname() +} + // EmailMessage represents a parsed email message type EmailMessage struct { Header mail.Header @@ -211,18 +218,18 @@ func buildRawHeaders(header mail.Header) string { } // GetAuthenticationResults extracts Authentication-Results headers -// If receiverHostname is provided, only returns headers that begin with that hostname -func (e *EmailMessage) GetAuthenticationResults(receiverHostname string) []string { +// If hostname is provided, only returns headers that begin with that hostname +func (e *EmailMessage) GetAuthenticationResults() []string { allResults := e.Header[textproto.CanonicalMIMEHeaderKey("Authentication-Results")] // If no hostname specified, return all results - if receiverHostname == "" { + if hostname == "" { return allResults } // Filter results that begin with the specified hostname var filtered []string - prefix := receiverHostname + ";" + prefix := hostname + ";" for _, result := range allResults { // Trim whitespace and check if it starts with hostname; trimmed := strings.TrimSpace(result) @@ -249,33 +256,6 @@ func (e *EmailMessage) GetSpamAssassinHeaders() map[string]string { } for _, headerName := range saHeaders { - if values, ok := e.Header[headerName]; ok && len(values) > 0 { - for _, value := range values { - if strings.TrimSpace(value) != "" { - headers[headerName] = value - break - } - } - } else if value := e.Header.Get(headerName); value != "" { - headers[headerName] = value - } - } - - return headers -} - -// GetRspamdHeaders extracts rspamd-related headers -func (e *EmailMessage) GetRspamdHeaders() map[string]string { - headers := make(map[string]string) - - rspamdHeaders := []string{ - "X-Spamd-Result", - "X-Rspamd-Score", - "X-Rspamd-Action", - "X-Rspamd-Server", - } - - for _, headerName := range rspamdHeaders { if value := e.Header.Get(headerName); value != "" { headers[headerName] = value } @@ -321,20 +301,3 @@ func (e *EmailMessage) GetHeaderValue(key string) string { func (e *EmailMessage) HasHeader(key string) bool { return e.Header.Get(key) != "" } - -// GetListUnsubscribeURLs parses the List-Unsubscribe header and returns all URLs. -// The header format is: , , ... -func (e *EmailMessage) GetListUnsubscribeURLs() []string { - value := e.Header.Get("List-Unsubscribe") - if value == "" { - return nil - } - var urls []string - for _, part := range strings.Split(value, ",") { - part = strings.TrimSpace(part) - if strings.HasPrefix(part, "<") && strings.HasSuffix(part, ">") { - urls = append(urls, part[1:len(part)-1]) - } - } - return urls -} diff --git a/pkg/analyzer/parser_test.go b/pkg/analyzer/parser_test.go index 196e8e2..571f542 100644 --- a/pkg/analyzer/parser_test.go +++ b/pkg/analyzer/parser_test.go @@ -120,7 +120,7 @@ Body content. t.Fatalf("Failed to parse email: %v", err) } - authResults := email.GetAuthenticationResults("example.com") + authResults := email.GetAuthenticationResults() if len(authResults) != 2 { t.Errorf("Expected 2 Authentication-Results headers, got: %d", len(authResults)) } diff --git a/pkg/analyzer/rbl.go b/pkg/analyzer/rbl.go index 47e74e0..5e8b503 100644 --- a/pkg/analyzer/rbl.go +++ b/pkg/analyzer/rbl.go @@ -27,21 +27,17 @@ import ( "net" "regexp" "strings" - "sync" "time" "git.happydns.org/happyDeliver/internal/api" ) -// DNSListChecker checks IP addresses against DNS-based block/allow lists. -// It handles both RBL (blacklist) and DNSWL (whitelist) semantics via flags. -type DNSListChecker struct { - Timeout time.Duration - Lists []string - CheckAllIPs bool // Check all IPs found in headers, not just the first one - filterErrorCodes bool // When true (RBL mode), treat 127.255.255.253/254/255 as operational errors - resolver *net.Resolver - informationalSet map[string]bool // Lists whose hits don't count toward the score +// RBLChecker checks IP addresses against DNS-based blacklists +type RBLChecker struct { + Timeout time.Duration + RBLs []string + CheckAllIPs bool // Check all IPs found in headers, not just the first one + resolver *net.Resolver } // DefaultRBLs is a list of commonly used RBL providers @@ -52,83 +48,40 @@ var DefaultRBLs = []string{ "b.barracudacentral.org", // Barracuda "cbl.abuseat.org", // CBL (Composite Blocking List) "dnsbl-1.uceprotect.net", // UCEPROTECT Level 1 - "dnsbl-2.uceprotect.net", // UCEPROTECT Level 2 (informational) - "dnsbl-3.uceprotect.net", // UCEPROTECT Level 3 (informational) - "psbl.surriel.com", // PSBL - "dnsbl.dronebl.org", // DroneBL - "bl.mailspike.net", // Mailspike BL - "z.mailspike.net", // Mailspike Z - "bl.rbl-dns.com", // RBL-DNS - "bl.nszones.com", // NSZones -} - -// DefaultInformationalRBLs lists RBLs that are checked but not counted in the score. -// These are typically broader lists where being listed is less definitive. -var DefaultInformationalRBLs = []string{ - "dnsbl-2.uceprotect.net", // UCEPROTECT Level 2: entire netblocks, may cause false positives - "dnsbl-3.uceprotect.net", // UCEPROTECT Level 3: entire ASes, too broad for scoring -} - -// DefaultDNSWLs is a list of commonly used DNSWL providers -var DefaultDNSWLs = []string{ - "list.dnswl.org", // DNSWL.org — the main DNS whitelist - "swl.spamhaus.org", // Spamhaus Safe Whitelist } // NewRBLChecker creates a new RBL checker with configurable timeout and RBL list -func NewRBLChecker(timeout time.Duration, rbls []string, checkAllIPs bool) *DNSListChecker { +func NewRBLChecker(timeout time.Duration, rbls []string, checkAllIPs bool) *RBLChecker { if timeout == 0 { - timeout = 5 * time.Second + timeout = 5 * time.Second // Default timeout } if len(rbls) == 0 { rbls = DefaultRBLs } - informationalSet := make(map[string]bool, len(DefaultInformationalRBLs)) - for _, rbl := range DefaultInformationalRBLs { - informationalSet[rbl] = true - } - return &DNSListChecker{ - Timeout: timeout, - Lists: rbls, - CheckAllIPs: checkAllIPs, - filterErrorCodes: true, - resolver: &net.Resolver{PreferGo: true}, - informationalSet: informationalSet, + return &RBLChecker{ + Timeout: timeout, + RBLs: rbls, + CheckAllIPs: checkAllIPs, + resolver: &net.Resolver{ + PreferGo: true, + }, } } -// NewDNSWLChecker creates a new DNSWL checker with configurable timeout and DNSWL list -func NewDNSWLChecker(timeout time.Duration, dnswls []string, checkAllIPs bool) *DNSListChecker { - if timeout == 0 { - timeout = 5 * time.Second - } - if len(dnswls) == 0 { - dnswls = DefaultDNSWLs - } - return &DNSListChecker{ - Timeout: timeout, - Lists: dnswls, - CheckAllIPs: checkAllIPs, - filterErrorCodes: false, - resolver: &net.Resolver{PreferGo: true}, - informationalSet: make(map[string]bool), - } +// RBLResults represents the results of RBL checks +type RBLResults struct { + Checks map[string][]api.BlacklistCheck // Map of IP -> list of RBL checks for that IP + IPsChecked []string + ListedCount int } -// DNSListResults represents the results of DNS list checks -type DNSListResults struct { - Checks map[string][]api.BlacklistCheck // Map of IP -> list of checks for that IP - IPsChecked []string - ListedCount int // Total listings including informational entries - RelevantListedCount int // Listings on scoring (non-informational) lists only -} - -// CheckEmail checks all IPs found in the email headers against the configured lists -func (r *DNSListChecker) CheckEmail(email *EmailMessage) *DNSListResults { - results := &DNSListResults{ +// CheckEmail checks all IPs found in the email headers against RBLs +func (r *RBLChecker) CheckEmail(email *EmailMessage) *RBLResults { + results := &RBLResults{ Checks: make(map[string][]api.BlacklistCheck), } + // Extract IPs from Received headers ips := r.extractIPs(email) if len(ips) == 0 { return results @@ -136,18 +89,17 @@ func (r *DNSListChecker) CheckEmail(email *EmailMessage) *DNSListResults { results.IPsChecked = ips + // Check each IP against all RBLs for _, ip := range ips { - for _, list := range r.Lists { - check := r.checkIP(ip, list) + for _, rbl := range r.RBLs { + check := r.checkIP(ip, rbl) results.Checks[ip] = append(results.Checks[ip], check) if check.Listed { results.ListedCount++ - if !r.informationalSet[list] { - results.RelevantListedCount++ - } } } + // Only check the first IP unless CheckAllIPs is enabled if !r.CheckAllIPs { break } @@ -156,48 +108,28 @@ func (r *DNSListChecker) CheckEmail(email *EmailMessage) *DNSListResults { return results } -// CheckIP checks a single IP address against all configured lists in parallel -func (r *DNSListChecker) CheckIP(ip string) ([]api.BlacklistCheck, int, error) { - if !r.isPublicIP(ip) { - return nil, 0, fmt.Errorf("invalid or non-public IP address: %s", ip) - } - - checks := make([]api.BlacklistCheck, len(r.Lists)) - var wg sync.WaitGroup - - for i, list := range r.Lists { - wg.Add(1) - go func(i int, list string) { - defer wg.Done() - checks[i] = r.checkIP(ip, list) - }(i, list) - } - wg.Wait() - - listedCount := 0 - for _, check := range checks { - if check.Listed { - listedCount++ - } - } - - return checks, listedCount, nil -} - // extractIPs extracts IP addresses from Received headers -func (r *DNSListChecker) extractIPs(email *EmailMessage) []string { +func (r *RBLChecker) extractIPs(email *EmailMessage) []string { var ips []string seenIPs := make(map[string]bool) + // Get all Received headers receivedHeaders := email.Header["Received"] + + // Regex patterns for IP addresses + // Match IPv4: xxx.xxx.xxx.xxx ipv4Pattern := regexp.MustCompile(`\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b`) + // Look for IPs in Received headers for _, received := range receivedHeaders { + // Find all IPv4 addresses matches := ipv4Pattern.FindAllString(received, -1) for _, match := range matches { + // Skip private/reserved IPs if !r.isPublicIP(match) { continue } + // Avoid duplicates if !seenIPs[match] { ips = append(ips, match) seenIPs[match] = true @@ -205,10 +137,13 @@ func (r *DNSListChecker) extractIPs(email *EmailMessage) []string { } } + // If no IPs found in Received headers, try X-Originating-IP if len(ips) == 0 { originatingIP := email.Header.Get("X-Originating-IP") if originatingIP != "" { + // Extract IP from formats like "[192.0.2.1]" or "192.0.2.1" cleanIP := strings.TrimSuffix(strings.TrimPrefix(originatingIP, "["), "]") + // Remove any whitespace cleanIP = strings.TrimSpace(cleanIP) matches := ipv4Pattern.FindString(cleanIP) if matches != "" && r.isPublicIP(matches) { @@ -221,16 +156,19 @@ func (r *DNSListChecker) extractIPs(email *EmailMessage) []string { } // isPublicIP checks if an IP address is public (not private, loopback, or reserved) -func (r *DNSListChecker) isPublicIP(ipStr string) bool { +func (r *RBLChecker) isPublicIP(ipStr string) bool { ip := net.ParseIP(ipStr) if ip == nil { return false } + // Check if it's a private network if ip.IsPrivate() || ip.IsLoopback() || ip.IsLinkLocalUnicast() || ip.IsLinkLocalMulticast() { return false } + // Additional checks for reserved ranges + // 0.0.0.0/8, 192.0.0.0/24, 192.0.2.0/24 (TEST-NET-1), 198.51.100.0/24 (TEST-NET-2), 203.0.113.0/24 (TEST-NET-3) if ip.IsUnspecified() { return false } @@ -238,43 +176,51 @@ func (r *DNSListChecker) isPublicIP(ipStr string) bool { return true } -// checkIP checks a single IP against a single DNS list -func (r *DNSListChecker) checkIP(ip, list string) api.BlacklistCheck { +// checkIP checks a single IP against a single RBL +func (r *RBLChecker) checkIP(ip, rbl string) api.BlacklistCheck { check := api.BlacklistCheck{ - Rbl: list, + Rbl: rbl, } + // Reverse the IP for DNSBL query reversedIP := r.reverseIP(ip) if reversedIP == "" { check.Error = api.PtrTo("Failed to reverse IP address") return check } - query := fmt.Sprintf("%s.%s", reversedIP, list) + // Construct DNSBL query: reversed-ip.rbl-domain + query := fmt.Sprintf("%s.%s", reversedIP, rbl) + // Perform DNS lookup with timeout ctx, cancel := context.WithTimeout(context.Background(), r.Timeout) defer cancel() addrs, err := r.resolver.LookupHost(ctx, query) if err != nil { + // Most likely not listed (NXDOMAIN) if dnsErr, ok := err.(*net.DNSError); ok { if dnsErr.IsNotFound { check.Listed = false return check } } + // Other DNS errors check.Error = api.PtrTo(fmt.Sprintf("DNS lookup failed: %v", err)) return check } + // If we got a response, check the return code if len(addrs) > 0 { - check.Response = api.PtrTo(addrs[0]) + check.Response = api.PtrTo(addrs[0]) // Return code (e.g., 127.0.0.2) - // In RBL mode, 127.255.255.253/254/255 indicate operational errors, not real listings. - if r.filterErrorCodes && (addrs[0] == "127.255.255.253" || addrs[0] == "127.255.255.254" || addrs[0] == "127.255.255.255") { + // Check for RBL error codes: 127.255.255.253, 127.255.255.254, 127.255.255.255 + // These indicate RBL operational issues, not actual listings + if addrs[0] == "127.255.255.253" || addrs[0] == "127.255.255.254" || addrs[0] == "127.255.255.255" { check.Listed = false - check.Error = api.PtrTo(fmt.Sprintf("RBL %s returned error code %s (RBL operational issue)", list, addrs[0])) + check.Error = api.PtrTo(fmt.Sprintf("RBL %s returned error code %s (RBL operational issue)", rbl, addrs[0])) } else { + // Normal listing response check.Listed = true } } @@ -282,58 +228,44 @@ func (r *DNSListChecker) checkIP(ip, list string) api.BlacklistCheck { return check } -// reverseIP reverses an IPv4 address for DNSBL/DNSWL queries +// reverseIP reverses an IPv4 address for DNSBL queries // Example: 192.0.2.1 -> 1.2.0.192 -func (r *DNSListChecker) reverseIP(ipStr string) string { +func (r *RBLChecker) reverseIP(ipStr string) string { ip := net.ParseIP(ipStr) if ip == nil { return "" } + // Convert to IPv4 ipv4 := ip.To4() if ipv4 == nil { return "" // IPv6 not supported yet } + // Reverse the octets return fmt.Sprintf("%d.%d.%d.%d", ipv4[3], ipv4[2], ipv4[1], ipv4[0]) } -// CalculateScore calculates the list contribution to deliverability. -// Informational lists are not counted in the score. -func (r *DNSListChecker) CalculateScore(results *DNSListResults, forWhitelist bool) (int, string) { - scoringListCount := len(r.Lists) - len(r.informationalSet) - - if forWhitelist { - if results.ListedCount >= scoringListCount { - return 100, "A++" - } else if results.ListedCount > 0 { - return 100, "A+" - } else { - return 95, "A" - } - } - +// CalculateRBLScore calculates the blacklist contribution to deliverability +func (r *RBLChecker) CalculateRBLScore(results *RBLResults) (int, string) { if results == nil || len(results.IPsChecked) == 0 { + // No IPs to check, give benefit of doubt return 100, "" } - if results.ListedCount <= 0 { - return 100, "A+" - } - - percentage := 100 - results.RelevantListedCount*100/scoringListCount + percentage := 100 - results.ListedCount*100/len(r.RBLs) return percentage, ScoreToGrade(percentage) } -// GetUniqueListedIPs returns a list of unique IPs that are listed on at least one entry -func (r *DNSListChecker) GetUniqueListedIPs(results *DNSListResults) []string { +// GetUniqueListedIPs returns a list of unique IPs that are listed on at least one RBL +func (r *RBLChecker) GetUniqueListedIPs(results *RBLResults) []string { var listedIPs []string - for ip, checks := range results.Checks { - for _, check := range checks { + for ip, rblChecks := range results.Checks { + for _, check := range rblChecks { if check.Listed { listedIPs = append(listedIPs, ip) - break + break // Only add the IP once } } } @@ -341,17 +273,17 @@ func (r *DNSListChecker) GetUniqueListedIPs(results *DNSListResults) []string { return listedIPs } -// GetListsForIP returns all lists that match a specific IP -func (r *DNSListChecker) GetListsForIP(results *DNSListResults, ip string) []string { - var lists []string +// GetRBLsForIP returns all RBLs that list a specific IP +func (r *RBLChecker) GetRBLsForIP(results *RBLResults, ip string) []string { + var rbls []string - if checks, exists := results.Checks[ip]; exists { - for _, check := range checks { + if rblChecks, exists := results.Checks[ip]; exists { + for _, check := range rblChecks { if check.Listed { - lists = append(lists, check.Rbl) + rbls = append(rbls, check.Rbl) } } } - return lists + return rbls } diff --git a/pkg/analyzer/rbl_test.go b/pkg/analyzer/rbl_test.go index 1dd1262..a1de270 100644 --- a/pkg/analyzer/rbl_test.go +++ b/pkg/analyzer/rbl_test.go @@ -59,8 +59,8 @@ func TestNewRBLChecker(t *testing.T) { if checker.Timeout != tt.expectedTimeout { t.Errorf("Timeout = %v, want %v", checker.Timeout, tt.expectedTimeout) } - if len(checker.Lists) != tt.expectedRBLs { - t.Errorf("RBLs count = %d, want %d", len(checker.Lists), tt.expectedRBLs) + if len(checker.RBLs) != tt.expectedRBLs { + t.Errorf("RBLs count = %d, want %d", len(checker.RBLs), tt.expectedRBLs) } if checker.resolver == nil { t.Error("Resolver should not be nil") @@ -265,7 +265,7 @@ func TestExtractIPs(t *testing.T) { func TestGetBlacklistScore(t *testing.T) { tests := []struct { name string - results *DNSListResults + results *RBLResults expectedScore int }{ { @@ -275,14 +275,14 @@ func TestGetBlacklistScore(t *testing.T) { }, { name: "No IPs checked", - results: &DNSListResults{ + results: &RBLResults{ IPsChecked: []string{}, }, expectedScore: 100, }, { name: "Not listed on any RBL", - results: &DNSListResults{ + results: &RBLResults{ IPsChecked: []string{"198.51.100.1"}, ListedCount: 0, }, @@ -290,7 +290,7 @@ func TestGetBlacklistScore(t *testing.T) { }, { name: "Listed on 1 RBL", - results: &DNSListResults{ + results: &RBLResults{ IPsChecked: []string{"198.51.100.1"}, ListedCount: 1, }, @@ -298,7 +298,7 @@ func TestGetBlacklistScore(t *testing.T) { }, { name: "Listed on 2 RBLs", - results: &DNSListResults{ + results: &RBLResults{ IPsChecked: []string{"198.51.100.1"}, ListedCount: 2, }, @@ -306,7 +306,7 @@ func TestGetBlacklistScore(t *testing.T) { }, { name: "Listed on 3 RBLs", - results: &DNSListResults{ + results: &RBLResults{ IPsChecked: []string{"198.51.100.1"}, ListedCount: 3, }, @@ -314,7 +314,7 @@ func TestGetBlacklistScore(t *testing.T) { }, { name: "Listed on 4+ RBLs", - results: &DNSListResults{ + results: &RBLResults{ IPsChecked: []string{"198.51.100.1"}, ListedCount: 4, }, @@ -326,7 +326,7 @@ func TestGetBlacklistScore(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - score, _ := checker.CalculateScore(tt.results) + score, _ := checker.CalculateRBLScore(tt.results) if score != tt.expectedScore { t.Errorf("GetBlacklistScore() = %v, want %v", score, tt.expectedScore) } @@ -335,7 +335,7 @@ func TestGetBlacklistScore(t *testing.T) { } func TestGetUniqueListedIPs(t *testing.T) { - results := &DNSListResults{ + results := &RBLResults{ Checks: map[string][]api.BlacklistCheck{ "198.51.100.1": { {Rbl: "zen.spamhaus.org", Listed: true}, @@ -363,7 +363,7 @@ func TestGetUniqueListedIPs(t *testing.T) { } func TestGetRBLsForIP(t *testing.T) { - results := &DNSListResults{ + results := &RBLResults{ Checks: map[string][]api.BlacklistCheck{ "198.51.100.1": { {Rbl: "zen.spamhaus.org", Listed: true}, @@ -402,7 +402,7 @@ func TestGetRBLsForIP(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - rbls := checker.GetListsForIP(results, tt.ip) + rbls := checker.GetRBLsForIP(results, tt.ip) if len(rbls) != len(tt.expectedRBLs) { t.Errorf("Got %d RBLs, want %d", len(rbls), len(tt.expectedRBLs)) diff --git a/pkg/analyzer/report.go b/pkg/analyzer/report.go index 7332307..a39a98a 100644 --- a/pkg/analyzer/report.go +++ b/pkg/analyzer/report.go @@ -33,31 +33,24 @@ import ( type ReportGenerator struct { authAnalyzer *AuthenticationAnalyzer spamAnalyzer *SpamAssassinAnalyzer - rspamdAnalyzer *RspamdAnalyzer dnsAnalyzer *DNSAnalyzer - rblChecker *DNSListChecker - dnswlChecker *DNSListChecker + rblChecker *RBLChecker contentAnalyzer *ContentAnalyzer headerAnalyzer *HeaderAnalyzer } // NewReportGenerator creates a new report generator func NewReportGenerator( - receiverHostname string, dnsTimeout time.Duration, httpTimeout time.Duration, rbls []string, - dnswls []string, checkAllIPs bool, - rspamdAPIURL string, ) *ReportGenerator { return &ReportGenerator{ - authAnalyzer: NewAuthenticationAnalyzer(receiverHostname), + authAnalyzer: NewAuthenticationAnalyzer(), spamAnalyzer: NewSpamAssassinAnalyzer(), - rspamdAnalyzer: NewRspamdAnalyzer(LoadRspamdSymbols(rspamdAPIURL)), dnsAnalyzer: NewDNSAnalyzer(dnsTimeout), rblChecker: NewRBLChecker(dnsTimeout, rbls, checkAllIPs), - dnswlChecker: NewDNSWLChecker(dnsTimeout, dnswls, checkAllIPs), contentAnalyzer: NewContentAnalyzer(httpTimeout), headerAnalyzer: NewHeaderAnalyzer(), } @@ -70,10 +63,8 @@ type AnalysisResults struct { Content *ContentResults DNS *api.DNSResults Headers *api.HeaderAnalysis - RBL *DNSListResults - DNSWL *DNSListResults + RBL *RBLResults SpamAssassin *api.SpamAssassinResult - Rspamd *api.RspamdResult } // AnalyzeEmail performs complete email analysis @@ -84,12 +75,10 @@ func (r *ReportGenerator) AnalyzeEmail(email *EmailMessage) *AnalysisResults { // Run all analyzers results.Authentication = r.authAnalyzer.AnalyzeAuthentication(email) - results.Headers = r.headerAnalyzer.GenerateHeaderAnalysis(email, results.Authentication) - results.DNS = r.dnsAnalyzer.AnalyzeDNS(email, results.Headers) + results.Headers = r.headerAnalyzer.GenerateHeaderAnalysis(email) + results.DNS = r.dnsAnalyzer.AnalyzeDNS(email, results.Authentication, results.Headers) results.RBL = r.rblChecker.CheckEmail(email) - results.DNSWL = r.dnswlChecker.CheckEmail(email) results.SpamAssassin = r.spamAnalyzer.AnalyzeSpamAssassin(email) - results.Rspamd = r.rspamdAnalyzer.AnalyzeRspamd(email) results.Content = r.contentAnalyzer.AnalyzeContent(email) return results @@ -141,32 +130,14 @@ func (r *ReportGenerator) GenerateReport(testID uuid.UUID, results *AnalysisResu blacklistScore := 0 var blacklistGrade string - var whitelistGrade string if results.RBL != nil { - blacklistScore, blacklistGrade = r.rblChecker.CalculateScore(results.RBL, false) - _, whitelistGrade = r.dnswlChecker.CalculateScore(results.DNSWL, true) + blacklistScore, blacklistGrade = r.rblChecker.CalculateRBLScore(results.RBL) } - saScore, saGrade := r.spamAnalyzer.CalculateSpamAssassinScore(results.SpamAssassin) - rspamdScore, rspamdGrade := r.rspamdAnalyzer.CalculateRspamdScore(results.Rspamd) - - // Combine SpamAssassin and rspamd scores 50/50. - // If only one filter ran (the other returns "" grade), use that filter's score alone. - var spamScore int + spamScore := 0 var spamGrade string - switch { - case saGrade == "" && rspamdGrade == "": - spamScore = 0 - spamGrade = "" - case saGrade == "": - spamScore = rspamdScore - spamGrade = rspamdGrade - case rspamdGrade == "": - spamScore = saScore - spamGrade = saGrade - default: - spamScore = (saScore + rspamdScore) / 2 - spamGrade = MinGrade(saGrade, rspamdGrade) + if results.SpamAssassin != nil { + spamScore, spamGrade = r.spamAnalyzer.CalculateSpamAssassinScore(results.SpamAssassin) } report.Summary = &api.ScoreSummary{ @@ -175,7 +146,7 @@ func (r *ReportGenerator) GenerateReport(testID uuid.UUID, results *AnalysisResu AuthenticationScore: authScore, AuthenticationGrade: api.ScoreSummaryAuthenticationGrade(authGrade), BlacklistScore: blacklistScore, - BlacklistGrade: api.ScoreSummaryBlacklistGrade(MinGrade(blacklistGrade, whitelistGrade)), + BlacklistGrade: api.ScoreSummaryBlacklistGrade(blacklistGrade), ContentScore: contentScore, ContentGrade: api.ScoreSummaryContentGrade(contentGrade), HeaderScore: headerScore, @@ -206,27 +177,9 @@ func (r *ReportGenerator) GenerateReport(testID uuid.UUID, results *AnalysisResu report.Blacklists = &results.RBL.Checks } - // Add whitelist checks as a map of IP -> array of BlacklistCheck (informational only) - if results.DNSWL != nil && len(results.DNSWL.Checks) > 0 { - report.Whitelists = &results.DNSWL.Checks - } - - // Add SpamAssassin result with individual deliverability score - if results.SpamAssassin != nil { - saGradeTyped := api.SpamAssassinResultDeliverabilityGrade(saGrade) - results.SpamAssassin.DeliverabilityScore = api.PtrTo(saScore) - results.SpamAssassin.DeliverabilityGrade = &saGradeTyped - } + // Add SpamAssassin result report.Spamassassin = results.SpamAssassin - // Add rspamd result with individual deliverability score - if results.Rspamd != nil { - rspamdGradeTyped := api.RspamdResultDeliverabilityGrade(rspamdGrade) - results.Rspamd.DeliverabilityScore = api.PtrTo(rspamdScore) - results.Rspamd.DeliverabilityGrade = &rspamdGradeTyped - } - report.Rspamd = results.Rspamd - // Add raw headers if results.Email != nil && results.Email.RawHeaders != "" { report.RawHeaders = &results.Email.RawHeaders diff --git a/pkg/analyzer/report_test.go b/pkg/analyzer/report_test.go index 5914737..5a325b1 100644 --- a/pkg/analyzer/report_test.go +++ b/pkg/analyzer/report_test.go @@ -32,7 +32,7 @@ import ( ) func TestNewReportGenerator(t *testing.T) { - gen := NewReportGenerator("", 10*time.Second, 10*time.Second, DefaultRBLs, DefaultDNSWLs, false, "") + gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, false) if gen == nil { t.Fatal("Expected report generator, got nil") } @@ -55,7 +55,7 @@ func TestNewReportGenerator(t *testing.T) { } func TestAnalyzeEmail(t *testing.T) { - gen := NewReportGenerator("", 10*time.Second, 10*time.Second, DefaultRBLs, DefaultDNSWLs, false, "") + gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, false) email := createTestEmail() @@ -75,7 +75,7 @@ func TestAnalyzeEmail(t *testing.T) { } func TestGenerateReport(t *testing.T) { - gen := NewReportGenerator("", 10*time.Second, 10*time.Second, DefaultRBLs, DefaultDNSWLs, false, "") + gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, false) testID := uuid.New() email := createTestEmail() @@ -130,7 +130,7 @@ func TestGenerateReport(t *testing.T) { } func TestGenerateReportWithSpamAssassin(t *testing.T) { - gen := NewReportGenerator("", 10*time.Second, 10*time.Second, DefaultRBLs, DefaultDNSWLs, false, "") + gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, false) testID := uuid.New() email := createTestEmailWithSpamAssassin() @@ -150,7 +150,7 @@ func TestGenerateReportWithSpamAssassin(t *testing.T) { } func TestGenerateRawEmail(t *testing.T) { - gen := NewReportGenerator("", 10*time.Second, 10*time.Second, DefaultRBLs, DefaultDNSWLs, false, "") + gen := NewReportGenerator(10*time.Second, 10*time.Second, DefaultRBLs, false) tests := []struct { name string diff --git a/pkg/analyzer/rspamd-symbols-README.md b/pkg/analyzer/rspamd-symbols-README.md deleted file mode 100644 index 882eab2..0000000 --- a/pkg/analyzer/rspamd-symbols-README.md +++ /dev/null @@ -1,21 +0,0 @@ -# rspamd-symbols.json - -This file contains rspamd symbol descriptions, embedded into the binary at compile time as a fallback when no rspamd API URL is configured. - -## How to update - -Fetch the latest symbols from a running rspamd instance: - -```sh -curl http://127.0.0.1:11334/symbols > rspamd-symbols.json -``` - -Or with docker: - -```sh -docker run --rm --name rspamd --pull always rspamd/rspamd -docker exec -u 0 rspamd apt install -y curl -docker exec rspamd curl http://127.0.0.1:11334/symbols > rspamd-symbols.json -``` - -Then rebuild the project. diff --git a/pkg/analyzer/rspamd-symbols.json b/pkg/analyzer/rspamd-symbols.json deleted file mode 100644 index 5538985..0000000 --- a/pkg/analyzer/rspamd-symbols.json +++ /dev/null @@ -1,6646 +0,0 @@ -[ - { - "group": "arc", - "rules": [ - { - "symbol": "ARC_ALLOW", - "weight": -1.0, - "description": "ARC checks success", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ARC_REJECT", - "weight": 1.0, - "description": "ARC checks failed", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ARC_NA", - "weight": 0.0, - "description": "ARC signature absent", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ARC_INVALID", - "weight": 0.500000, - "description": "ARC structure invalid", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ARC_CHECK", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ARC_DNSFAIL", - "weight": 0.0, - "description": "ARC DNS error", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ARC_SIGNED", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "rbl", - "rules": [ - { - "symbol": "RBL_SENDERSCORE_SUS_ATT_NA_BOT", - "weight": 1.500000, - "description": "From address is listed in SenderScore RPBL - suspect_attachments+noauth+botnet" - }, - { - "symbol": "RBL_SENDERSCORE_REPUT_0", - "weight": 4.0, - "description": "SenderScore Reputation: Very Bad (0-9).", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_REPUT_2", - "weight": 3.0, - "description": "SenderScore Reputation: Bad (20-29).", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URIBL_RED", - "weight": 0.500000, - "description": "A domain in the message is listed in URIBL.com red", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RECEIVED_SPAMHAUS_BLOCKED", - "weight": 0.0, - "description": "You are exceeding the query limit, please see https://www.spamhaus.org/returnc/vol/", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_PRST_NA", - "weight": 2.0, - "description": "From address is listed in SenderScore RPBL - pristine+noauth" - }, - { - "symbol": "RECEIVED_SPAMHAUS", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RECEIVED_SPAMHAUS_CSS", - "weight": 1.0, - "description": "Received address is listed in Spamhaus CSS", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DWL_DNSWL_BLOCKED", - "weight": 0.0, - "description": "https://www.dnswl.org: Resolver blocked due to excessive queries (DWL)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_UNKNOWN", - "weight": 0.0, - "description": "Unrecognised result from SenderScore RPBL" - }, - { - "symbol": "RBL_VIRUSFREE_BOTNET", - "weight": 2.0, - "description": "From address is listed in virusfree.cz BL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DWL_DNSWL_HI", - "weight": -3.500000, - "description": "Message has a valid dkim signature originated from domain listed at https://www.dnswl.org, high trust", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_VIRUSFREE_UNKNOWN", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SPAMHAUS_BLOCKED", - "weight": 0.0, - "description": "You are exceeding the query limit, please see https://www.spamhaus.org/returnc/vol/", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_MAILSPIKE_BAD", - "weight": 1.0, - "description": "From address is listed in Mailspike RBL - bad reputation", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SPAMHAUS_SBL", - "weight": 4.0, - "description": "From address is listed in Spamhaus SBL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RECEIVED_BLOCKLISTDE", - "weight": 3.0, - "description": "Received address is listed in Blocklist (https://www.blocklist.de/)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_BLOCKED_OPENRESOLVER", - "weight": 0.0, - "description": "You are querying Spamhaus from an open resolver, please see https://www.spamhaus.org/returnc/pub/", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "CRACKED_SURBL", - "weight": 5.0, - "description": "A domain in the message is listed in SURBL as cracked", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SURBL_HASHBL_CRACKED", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_BLOCKED", - "weight": 0.0, - "description": "Excessive number of queries to SenderScore RPBL, more info: https://knowledge.validity.com/hc/en-us/articles/20961730681243" - }, - { - "symbol": "RBL_SENDERSCORE_REPUT_4", - "weight": 2.0, - "description": "SenderScore Reputation: Bad (40-49).", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "PH_SURBL_MULTI", - "weight": 7.500000, - "description": "A domain in the message is listed in SURBL as phishing", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_SUS_ATT_PRST_NA_BOT", - "weight": 3.500000, - "description": "From address is listed in SenderScore RPBL - suspect_attachments+pristine+noauth+botnet" - }, - { - "symbol": "RBL_SENDERSCORE_SUS_ATT", - "weight": 1.0, - "description": "From address is listed in SenderScore RPBL - suspect_attachments" - }, - { - "symbol": "RBL_SENDERSCORE_REPUT_8", - "weight": 0.0, - "description": "SenderScore Reputation: Neutral (80-89).", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SPAMHAUS_BLOCKED_OPENRESOLVER", - "weight": 0.0, - "description": "You are querying Spamhaus from an open resolver, please see https://www.spamhaus.org/returnc/pub/", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCVD_IN_DNSWL_MED", - "weight": -0.200000, - "description": "Sender listed at https://www.dnswl.org, medium trust", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DWL_DNSWL_NONE", - "weight": 0.0, - "description": "Message has a valid dkim signature originated from domain listed at https://www.dnswl.org, no trust", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MSBL_EBL", - "weight": 7.500000, - "description": "MSBL emailbl (https://www.msbl.org/)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SPAMHAUS_XBL", - "weight": 4.0, - "description": "From address is listed in Spamhaus XBL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_SUS_ATT_NA", - "weight": 1.0, - "description": "From address is listed in SenderScore RPBL - suspect_attachments+noauth" - }, - { - "symbol": "RBL_SENDERSCORE_PRST_BOT", - "weight": 3.0, - "description": "From address is listed in SenderScore RPBL - pristine+botnet" - }, - { - "symbol": "SURBL_HASHBL", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_PRST_NA_BOT", - "weight": 3.0, - "description": "From address is listed in SenderScore RPBL - pristine+noauth+botnet" - }, - { - "symbol": "RECEIVED_SPAMHAUS_SBL", - "weight": 3.0, - "description": "Received address is listed in Spamhaus SBL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RWL_MAILSPIKE_POSSIBLE", - "weight": 0.0, - "description": "From address is listed in Mailspike RWL - possibly legit", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCVD_IN_DNSWL_HI", - "weight": -0.500000, - "description": "Sender listed at https://www.dnswl.org, high trust", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SPAMHAUS_PBL", - "weight": 2.0, - "description": "From address is listed in Spamhaus PBL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DWL_DNSWL_LOW", - "weight": -1.0, - "description": "Message has a valid dkim signature originated from domain listed at https://www.dnswl.org, low trust", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_REPUT_BLOCKED", - "weight": 0.0, - "description": "Excessive number of queries to SenderScore RPBL, more info: https://knowledge.validity.com/hc/en-us/articles/20961730681243", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_REPUT_7", - "weight": 0.500000, - "description": "SenderScore Reputation: Bad (70-79).", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SEM_URIBL_FRESH15_UNKNOWN", - "weight": 0.0, - "description": "Unrecognised result from Spameatingmonkey Fresh15 URIBL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SURBL_HASHBL_MALWARE", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_ABUSE_MALWARE", - "weight": 6.500000, - "description": "A domain in the message is listed in Spamhaus DBL as abused legit malware", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_BLOCKLISTDE", - "weight": 4.0, - "description": "From address is listed in Blocklist (https://www.blocklist.de/)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_SPAM", - "weight": 6.500000, - "description": "A domain in the message is listed in Spamhaus DBL as spam", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ABUSE_SURBL", - "weight": 5.0, - "description": "A domain in the message is listed in SURBL as abused", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_MALWARE", - "weight": 7.500000, - "description": "A domain in the message is listed in Spamhaus DBL as malware", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SURBL_HASHBL_PHISH", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RECEIVED_SPAMHAUS_DROP", - "weight": 6.0, - "description": "Received address is listed in Spamhaus DROP", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_SCORE_NA", - "weight": 2.0, - "description": "From address is listed in SenderScore RPBL - sender_score+noauth" - }, - { - "symbol": "DBL_ABUSE_REDIR", - "weight": 5.0, - "description": "A domain in the message is listed in Spamhaus DBL as spammed redirector domain", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "CT_SURBL", - "weight": 0.0, - "description": "A domain in the message is listed in SURBL as a clicktracker", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SURBL_HASHBL_EMAIL", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_SCORE_SUS_ATT_NA", - "weight": 3.0, - "description": "From address is listed in SenderScore RPBL - sender_score+suspect_attachments+noauth" - }, - { - "symbol": "RECEIVED_SPAMHAUS_XBL", - "weight": 1.0, - "description": "Received address is listed in Spamhaus XBL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_BLOCKED", - "weight": 0.0, - "description": "You are exceeding the query limit, please see https://www.spamhaus.org/returnc/vol/", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RWL_MAILSPIKE_GOOD", - "weight": -0.100000, - "description": "From address is listed in Mailspike RWL - good reputation", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_SCORE_PRST", - "weight": 4.0, - "description": "From address is listed in SenderScore RPBL - sender_score+pristine" - }, - { - "symbol": "RBL_MAILSPIKE_VERYBAD", - "weight": 1.500000, - "description": "From address is listed in Mailspike RBL - very bad reputation", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SEM_IPV6", - "weight": 1.0, - "description": "From address is listed in Spameatingmonkey RBL (IPv6)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MW_SURBL_MULTI", - "weight": 7.500000, - "description": "A domain in the message is listed in SURBL as malware", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_NA", - "weight": 0.0, - "description": "From address is listed in SenderScore RPBL - noauth" - }, - { - "symbol": "RBL_SENDERSCORE_REPUT_9", - "weight": -1.0, - "description": "SenderScore Reputation: Good (90-100).", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URIBL_BLOCKED", - "weight": 0.0, - "description": "URIBL.com: query refused, likely due to policy/overusage", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URIBL_GREY", - "weight": 2.500000, - "description": "A domain in the message is listed in URIBL.com grey", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SURBL_BLOCKED", - "weight": 0.0, - "description": "SURBL: query blocked by policy/overusage", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCVD_IN_DNSWL_LOW", - "weight": -0.100000, - "description": "Sender listed at https://www.dnswl.org, low trust", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_ABUSE_PHISH", - "weight": 6.500000, - "description": "A domain in the message is listed in Spamhaus DBL as abused legit phish", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCVD_IN_DNSWL_NONE", - "weight": 0.0, - "description": "Sender listed at https://www.dnswl.org, no trust", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_SCORE_PRST_NA", - "weight": 4.0, - "description": "From address is listed in SenderScore RPBL - sender_score+pristine+noauth" - }, - { - "symbol": "MSBL_EBL_GREY", - "weight": 0.500000, - "description": "MSBL emailbl grey list (https://www.msbl.org/)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_REPUT_1", - "weight": 3.500000, - "description": "SenderScore Reputation: Bad (10-19).", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_BOT", - "weight": 2.0, - "description": "From address is listed in SenderScore RPBL - botnet" - }, - { - "symbol": "SEM_URIBL_UNKNOWN", - "weight": 0.0, - "description": "Unrecognised result from Spameatingmonkey URIBL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RWL_MAILSPIKE_NEUTRAL", - "weight": 0.0, - "description": "Neutral result from Mailspike", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SURBL_HASHBL_ABUSE", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_ABUSE", - "weight": 5.0, - "description": "A domain in the message is listed in Spamhaus DBL as abused legit spam", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_REPUT_6", - "weight": 1.0, - "description": "SenderScore Reputation: Bad (60-69).", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SEM_URIBL", - "weight": 3.500000, - "description": "A domain in the message is listed in Spameatingmonkey URIBL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RECEIVED_SPAMHAUS_PBL", - "weight": 0.0, - "description": "Received address is listed in Spamhaus PBL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DM_SURBL", - "weight": 0.0, - "description": "A domain in the message is listed in SURBL as belonging to a disposable email service", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_REPUT_5", - "weight": 1.500000, - "description": "SenderScore Reputation: Bad (50-59).", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_MAILSPIKE_WORST", - "weight": 2.0, - "description": "From address is listed in Mailspike RBL - worst possible reputation", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_ABUSE_BOTNET", - "weight": 6.500000, - "description": "A domain in the message is listed in Spamhaus DBL as abused legit botnet C&C", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_SUS_ATT_PRST_NA", - "weight": 3.0, - "description": "From address is listed in SenderScore RPBL - suspect_attachments+pristine+noauth" - }, - { - "symbol": "DWL_DNSWL", - "weight": 0.0, - "description": "Unrecognised result from https://www.dnswl.org (DWL)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SPAMHAUS_CSS", - "weight": 2.0, - "description": "From address is listed in Spamhaus CSS", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_PRST", - "weight": 2.0, - "description": "From address is listed in SenderScore RPBL - pristine" - }, - { - "symbol": "DWL_DNSWL_MED", - "weight": -2.0, - "description": "Message has a valid dkim signature originated from domain listed at https://www.dnswl.org, medium trust", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SPAMHAUS_DROP", - "weight": 7.0, - "description": "From address is listed in Spamhaus DROP", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_REPUT_UNKNOWN", - "weight": 0.0, - "description": "Unrecognized result from SenderScore Reputation list.", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL", - "weight": 0.0, - "description": "Unrecognised result from Spamhaus DBL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MAILSPIKE", - "weight": 0.0, - "description": "Unrecognised result from Mailspike", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_SCORE", - "weight": 2.0, - "description": "From address is listed in SenderScore RPBL - sender_score" - }, - { - "symbol": "RBL_SPAMHAUS", - "weight": 0.0, - "description": "Unrecognised result from Spamhaus ZEN", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DNSWL_BLOCKED", - "weight": 0.0, - "description": "https://www.dnswl.org: Resolver blocked due to excessive queries", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCVD_IN_DNSWL", - "weight": 0.0, - "description": "Unrecognised result from https://www.dnswl.org", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RWL_MAILSPIKE_VERYGOOD", - "weight": -0.200000, - "description": "From address is listed in Mailspike RWL - very good reputation", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RECEIVED_SPAMHAUS_BLOCKED_OPENRESOLVER", - "weight": 0.0, - "description": "You are querying Spamhaus from an open resolver, please see https://www.spamhaus.org/returnc/pub/", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_REPUT_3", - "weight": 2.500000, - "description": "SenderScore Reputation: Bad (30-39).", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URIBL_MULTI", - "weight": 0.0, - "description": "Unrecognised result from URIBL.com", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SEM_URIBL_FRESH15", - "weight": 3.0, - "description": "A domain in the message is listed in Spameatingmonkey Fresh15 URIBL (registered in the past 15 days, .AERO,.BIZ,.COM,.INFO,.NAME,.NET,.PRO,.SK,.TEL,.US only)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SEM", - "weight": 1.0, - "description": "From address is listed in Spameatingmonkey RBL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RWL_MAILSPIKE_EXCELLENT", - "weight": -0.400000, - "description": "From address is listed in Mailspike RWL - excellent reputation", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RSPAMD_EMAILBL", - "weight": 2.500000, - "description": "Rspamd emailbl, bl.rspamd.com", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URIBL_BLACK", - "weight": 7.500000, - "description": "A domain in the message is listed in URIBL.com black", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RSPAMD_URIBL", - "weight": 4.500000, - "description": "Rspamd uribl, bl.rspamd.com", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SURBL_MULTI", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_NA_BOT", - "weight": 1.0, - "description": "From address is listed in SenderScore RPBL - noauth+botnet" - }, - { - "symbol": "DBL_PROHIBIT", - "weight": 0.0, - "description": "DBL uribl IP queries prohibited!", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_BOTNET", - "weight": 7.500000, - "description": "A domain in the message is listed in Spamhaus DBL as botnet C&C", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_PHISH", - "weight": 7.500000, - "description": "A domain in the message is listed in Spamhaus DBL as phishing", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "dnswl", - "rules": [ - { - "symbol": "RCVD_IN_DNSWL_MED", - "weight": -0.200000, - "description": "Sender listed at https://www.dnswl.org, medium trust", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCVD_IN_DNSWL_LOW", - "weight": -0.100000, - "description": "Sender listed at https://www.dnswl.org, low trust", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCVD_IN_DNSWL_NONE", - "weight": 0.0, - "description": "Sender listed at https://www.dnswl.org, no trust", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DWL_DNSWL", - "weight": 0.0, - "description": "Unrecognised result from https://www.dnswl.org (DWL)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCVD_IN_DNSWL", - "weight": 0.0, - "description": "Unrecognised result from https://www.dnswl.org", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DNSWL_BLOCKED", - "weight": 0.0, - "description": "https://www.dnswl.org: Resolver blocked due to excessive queries", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DWL_DNSWL_BLOCKED", - "weight": 0.0, - "description": "https://www.dnswl.org: Resolver blocked due to excessive queries (DWL)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCVD_IN_DNSWL_HI", - "weight": -0.500000, - "description": "Sender listed at https://www.dnswl.org, high trust", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DWL_DNSWL_LOW", - "weight": -1.0, - "description": "Message has a valid dkim signature originated from domain listed at https://www.dnswl.org, low trust", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DWL_DNSWL_NONE", - "weight": 0.0, - "description": "Message has a valid dkim signature originated from domain listed at https://www.dnswl.org, no trust", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DWL_DNSWL_HI", - "weight": -3.500000, - "description": "Message has a valid dkim signature originated from domain listed at https://www.dnswl.org, high trust", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DWL_DNSWL_MED", - "weight": -2.0, - "description": "Message has a valid dkim signature originated from domain listed at https://www.dnswl.org, medium trust", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "dmarc", - "rules": [ - { - "symbol": "DMARC_POLICY_ALLOW", - "weight": -0.500000, - "description": "DMARC permit policy", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "BLACKLIST_DMARC", - "weight": 6.0, - "description": "Mail comes from the whitelisted domain and has failed DMARC and DKIM policies", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DMARC_POLICY_REJECT", - "weight": 2.0, - "description": "DMARC reject policy", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DMARC_POLICY_ALLOW_WITH_FAILURES", - "weight": -0.500000, - "description": "DMARC permit policy with DKIM/SPF failure", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DMARC_POLICY_SOFTFAIL", - "weight": 0.100000, - "description": "DMARC failed", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "WHITELIST_DMARC", - "weight": -7.0, - "description": "Mail comes from the whitelisted domain and has valid DMARC and DKIM policies", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DMARC_NA", - "weight": 0.0, - "description": "No DMARC record", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DMARC_POLICY_QUARANTINE", - "weight": 1.500000, - "description": "DMARC quarantine policy", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DMARC_DNSFAIL", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DMARC_BAD_POLICY", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "statistics", - "rules": [ - { - "symbol": "BAYES_SPAM", - "weight": 5.100000, - "description": "Message probably spam, probability: ", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "BAYES_HAM", - "weight": -3.0, - "description": "Message probably ham, probability: ", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "dkim", - "rules": [ - { - "symbol": "R_DKIM_ALLOW", - "weight": -0.200000, - "description": "DKIM verification succeed", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "WHITELIST_DKIM", - "weight": -1.0, - "description": "Mail comes from the whitelisted domain and has a valid DKIM signature", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_DKIM_REJECT", - "weight": 1.0, - "description": "DKIM verification failed", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "WHITELIST_SPF_DKIM", - "weight": -3.0, - "description": "Mail comes from the whitelisted domain and has valid SPF and DKIM policies", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "BLACKLIST_DMARC", - "weight": 6.0, - "description": "Mail comes from the whitelisted domain and has failed DMARC and DKIM policies", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_DKIM_TEMPFAIL", - "weight": 0.0, - "description": "DKIM verification soft-failed", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DKIM_CHECK", - "weight": 0.0, - "description": "DKIM check callback", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "BLACKLIST_DKIM", - "weight": 2.0, - "description": "Mail comes from the whitelisted domain and has non-valid DKIM signature", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_DKIM_PERMFAIL", - "weight": 0.0, - "description": "DKIM verification hard-failed (invalid)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "BLACKLIST_SPF_DKIM", - "weight": 3.0, - "description": "Mail comes from the whitelisted domain and has no valid SPF policy or a bad DKIM signature", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_DKIM_NA", - "weight": 0.0, - "description": "Missing DKIM signature", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DKIM_SIGNED", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DKIM_TRACE", - "weight": 0.0, - "description": "DKIM trace symbol", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "WHITELIST_DMARC", - "weight": -7.0, - "description": "Mail comes from the whitelisted domain and has valid DMARC and DKIM policies", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "sem", - "rules": [ - { - "symbol": "SEM_URIBL_FRESH15_UNKNOWN", - "weight": 0.0, - "description": "Unrecognised result from Spameatingmonkey Fresh15 URIBL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SEM_URIBL_FRESH15", - "weight": 3.0, - "description": "A domain in the message is listed in Spameatingmonkey Fresh15 URIBL (registered in the past 15 days, .AERO,.BIZ,.COM,.INFO,.NAME,.NET,.PRO,.SK,.TEL,.US only)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SEM_URIBL", - "weight": 3.500000, - "description": "A domain in the message is listed in Spameatingmonkey URIBL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SEM", - "weight": 1.0, - "description": "From address is listed in Spameatingmonkey RBL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SEM_IPV6", - "weight": 1.0, - "description": "From address is listed in Spameatingmonkey RBL (IPv6)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SEM_URIBL_UNKNOWN", - "weight": 0.0, - "description": "Unrecognised result from Spameatingmonkey URIBL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "neural", - "rules": [] - }, - { - "group": "policies", - "rules": [ - { - "symbol": "R_SPF_NA", - "weight": 0.0, - "description": "Missing SPF record", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_DKIM_TEMPFAIL", - "weight": 0.0, - "description": "DKIM verification soft-failed", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DMARC_POLICY_SOFTFAIL", - "weight": 0.100000, - "description": "DMARC failed", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ARC_ALLOW", - "weight": -1.0, - "description": "ARC checks success", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ARC_SIGNED", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_SPF_ALLOW", - "weight": -0.200000, - "description": "SPF verification allows sending", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_DKIM_NA", - "weight": 0.0, - "description": "Missing DKIM signature", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DMARC_BAD_POLICY", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SPF_CHECK", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DMARC_NA", - "weight": 0.0, - "description": "No DMARC record", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DMARC_POLICY_ALLOW_WITH_FAILURES", - "weight": -0.500000, - "description": "DMARC permit policy with DKIM/SPF failure", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_SPF_PLUSALL", - "weight": 4.0, - "description": "SPF record allows to send from any IP", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_SPF_SOFTFAIL", - "weight": 0.0, - "description": "SPF verification soft-failed", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ARC_INVALID", - "weight": 0.500000, - "description": "ARC structure invalid", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DMARC_DNSFAIL", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_DKIM_PERMFAIL", - "weight": 0.0, - "description": "DKIM verification hard-failed (invalid)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DKIM_TRACE", - "weight": 0.0, - "description": "DKIM trace symbol", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DMARC_POLICY_ALLOW", - "weight": -0.500000, - "description": "DMARC permit policy", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DKIM_CHECK", - "weight": 0.0, - "description": "DKIM check callback", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ARC_DNSFAIL", - "weight": 0.0, - "description": "ARC DNS error", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ARC_REJECT", - "weight": 1.0, - "description": "ARC checks failed", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_SPF_PERMFAIL", - "weight": 0.0, - "description": "SPF record is malformed or persistent DNS error", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ARC_NA", - "weight": 0.0, - "description": "ARC signature absent", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_SPF_NEUTRAL", - "weight": 0.0, - "description": "SPF policy is neutral", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DMARC_POLICY_QUARANTINE", - "weight": 1.500000, - "description": "DMARC quarantine policy", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_SPF_FAIL", - "weight": 1.0, - "description": "SPF verification failed", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_SPF_DNSFAIL", - "weight": 0.0, - "description": "SPF DNS failure", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DMARC_POLICY_REJECT", - "weight": 2.0, - "description": "DMARC reject policy", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_DKIM_ALLOW", - "weight": -0.200000, - "description": "DKIM verification succeed", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_DKIM_REJECT", - "weight": 1.0, - "description": "DKIM verification failed", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DKIM_SIGNED", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ARC_CHECK", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "surbl", - "rules": [ - { - "symbol": "DBL_BLOCKED", - "weight": 0.0, - "description": "You are exceeding the query limit, please see https://www.spamhaus.org/returnc/vol/", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_ABUSE_BOTNET", - "weight": 6.500000, - "description": "A domain in the message is listed in Spamhaus DBL as abused legit botnet C&C", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_PROHIBIT", - "weight": 0.0, - "description": "DBL uribl IP queries prohibited!", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SPAMHAUS_ZEN_URIBL", - "weight": 0.0, - "description": "Unrecognised result from Spamhaus ZEN URIBL" - }, - { - "symbol": "MSBL_EBL", - "weight": 7.500000, - "description": "MSBL emailbl (https://www.msbl.org/)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_ABUSE", - "weight": 5.0, - "description": "A domain in the message is listed in Spamhaus DBL as abused legit spam", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "PH_SURBL_MULTI", - "weight": 7.500000, - "description": "A domain in the message is listed in SURBL as phishing", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_BOTNET", - "weight": 7.500000, - "description": "A domain in the message is listed in Spamhaus DBL as botnet C&C", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RSPAMD_EMAILBL", - "weight": 2.500000, - "description": "Rspamd emailbl, bl.rspamd.com", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SEM_URIBL_UNKNOWN", - "weight": 0.0, - "description": "Unrecognised result from Spameatingmonkey URIBL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_BLOCKED_OPENRESOLVER", - "weight": 0.0, - "description": "You are querying Spamhaus from an open resolver, please see https://www.spamhaus.org/returnc/pub/", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "CT_SURBL", - "weight": 0.0, - "description": "A domain in the message is listed in SURBL as a clicktracker", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SEM_URIBL", - "weight": 3.500000, - "description": "A domain in the message is listed in Spameatingmonkey URIBL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RSPAMD_URIBL", - "weight": 4.500000, - "description": "Rspamd uribl, bl.rspamd.com", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SEM_URIBL_FRESH15_UNKNOWN", - "weight": 0.0, - "description": "Unrecognised result from Spameatingmonkey Fresh15 URIBL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URIBL_SBL", - "weight": 6.500000, - "description": "A domain in the message body resolves to an IP listed in Spamhaus SBL" - }, - { - "symbol": "URIBL_BLACK", - "weight": 7.500000, - "description": "A domain in the message is listed in URIBL.com black", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ABUSE_SURBL", - "weight": 5.0, - "description": "A domain in the message is listed in SURBL as abused", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_ABUSE_REDIR", - "weight": 5.0, - "description": "A domain in the message is listed in Spamhaus DBL as spammed redirector domain", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URIBL_PBL", - "weight": 0.010000, - "description": "A domain in the message body resolves to an IP listed in Spamhaus PBL" - }, - { - "symbol": "DBL_ABUSE_PHISH", - "weight": 6.500000, - "description": "A domain in the message is listed in Spamhaus DBL as abused legit phish", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MSBL_EBL_GREY", - "weight": 0.500000, - "description": "MSBL emailbl grey list (https://www.msbl.org/)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_SPAM", - "weight": 6.500000, - "description": "A domain in the message is listed in Spamhaus DBL as spam", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "CRACKED_SURBL", - "weight": 5.0, - "description": "A domain in the message is listed in SURBL as cracked", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URIBL_GREY", - "weight": 2.500000, - "description": "A domain in the message is listed in URIBL.com grey", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URIBL_RED", - "weight": 0.500000, - "description": "A domain in the message is listed in URIBL.com red", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URIBL_DROP", - "weight": 5.0, - "description": "A domain in the message body resolves to an IP listed in Spamhaus DROP" - }, - { - "symbol": "DBL_PHISH", - "weight": 7.500000, - "description": "A domain in the message is listed in Spamhaus DBL as phishing", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URIBL_MULTI", - "weight": 0.0, - "description": "Unrecognised result from URIBL.com", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_ABUSE_MALWARE", - "weight": 6.500000, - "description": "A domain in the message is listed in Spamhaus DBL as abused legit malware", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL", - "weight": 0.0, - "description": "Unrecognised result from Spamhaus DBL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_MALWARE", - "weight": 7.500000, - "description": "A domain in the message is listed in Spamhaus DBL as malware", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MW_SURBL_MULTI", - "weight": 7.500000, - "description": "A domain in the message is listed in SURBL as malware", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URIBL_XBL", - "weight": 3.0, - "description": "A domain in the message body resolves to an IP listed in Spamhaus XBL" - }, - { - "symbol": "SEM_URIBL_FRESH15", - "weight": 3.0, - "description": "A domain in the message is listed in Spameatingmonkey Fresh15 URIBL (registered in the past 15 days, .AERO,.BIZ,.COM,.INFO,.NAME,.NET,.PRO,.SK,.TEL,.US only)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URIBL_SBL_CSS", - "weight": 5.0, - "description": "A domain in the message body resolves to an IP listed in Spamhaus CSS" - }, - { - "symbol": "DM_SURBL", - "weight": 0.0, - "description": "A domain in the message is listed in SURBL as belonging to a disposable email service", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URIBL_BLOCKED", - "weight": 0.0, - "description": "URIBL.com: query refused, likely due to policy/overusage", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SURBL_BLOCKED", - "weight": 0.0, - "description": "SURBL: query blocked by policy/overusage", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "mime", - "rules": [ - { - "symbol": "MIME_BASE64_TEXT_BOGUS", - "weight": 1.0, - "description": "Has text part encoded in base64 that does not contain any 8bit characters", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "CTYPE_MIXED_BOGUS", - "weight": 1.0, - "description": "multipart/mixed without non-textual part", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "CTYPE_MISSING_DISPOSITION", - "weight": 4.0, - "description": "Binary content-type not specified as an attachment", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MIME_BASE64_TEXT", - "weight": 0.100000, - "description": "Has text part encoded in base64", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "multimap", - "rules": [ - { - "symbol": "DISPOSABLE_FROM", - "weight": 0.0, - "description": "From a Disposable e-mail address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DISPOSABLE_ENVFROM", - "weight": 0.0, - "description": "Envelope From is a Disposable e-mail address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DISPOSABLE_TO", - "weight": 0.0, - "description": "To a disposable e-mail address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DISPOSABLE_REPLYTO", - "weight": 0.0, - "description": "Reply-To a disposable e-mail address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DISPOSABLE_CC", - "weight": 0.0, - "description": "To a disposable e-mail address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FREEMAIL_TO", - "weight": 0.0, - "description": "To is a Freemail address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FREEMAIL_ENVRCPT", - "weight": 0.0, - "description": "Envelope Recipient is a Freemail address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FREEMAIL_ENVFROM", - "weight": 0.0, - "description": "Envelope From is a Freemail address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DISPOSABLE_MDN", - "weight": 0.500000, - "description": "Disposition-Notification-To is a disposable e-mail address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FREEMAIL_MDN", - "weight": 0.0, - "description": "Disposition-Notification-To is a Freemail address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FREEMAIL_FROM", - "weight": 0.0, - "description": "From is a Freemail address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FREEMAIL_REPLYTO", - "weight": 0.0, - "description": "Reply-To is a Freemail address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DISPOSABLE_ENVRCPT", - "weight": 0.0, - "description": "Envelope Recipient is a Disposable e-mail address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FREEMAIL_CC", - "weight": 0.0, - "description": "To is a Freemail address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "REDIRECTOR_URL", - "weight": 0.0, - "description": "The presence of a redirector in the mail", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "excessqp", - "rules": [ - { - "symbol": "CC_EXCESS_QP", - "weight": 1.200000, - "description": "Cc header is unnecessarily encoded in quoted-printable", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SUBJ_EXCESS_QP", - "weight": 1.200000, - "description": "Subject header is unnecessarily encoded in quoted-printable", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "REPLYTO_EXCESS_QP", - "weight": 1.200000, - "description": "Reply-To header is unnecessarily encoded in quoted-printable", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FROM_EXCESS_QP", - "weight": 1.200000, - "description": "From header is unnecessarily encoded in quoted-printable", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "TO_EXCESS_QP", - "weight": 1.200000, - "description": "To header is unnecessarily encoded in quoted-printable", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "upstream_spam_filters", - "rules": [ - { - "symbol": "UNITEDINTERNET_SPAM", - "weight": 5.0, - "description": "United Internet says this message is spam", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "KLMS_SPAM", - "weight": 5.0, - "description": "Kaspersky Security for Mail Server says this message is spam", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MICROSOFT_SPAM", - "weight": 4.0, - "description": "Microsoft says the message is spam", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "PRECEDENCE_BULK", - "weight": 0.0, - "description": "Message marked as bulk", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SPAM_FLAG", - "weight": 5.0, - "description": "Message was already marked as spam", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "headers", - "rules": [ - { - "symbol": "FAKE_RECEIVED_smtp_yandex_ru", - "weight": 4.0, - "description": "Fake smtp.yandex.ru Received header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HEADER_RCONFIRM_MISMATCH", - "weight": 2.0, - "description": "Read confirmation address is different to from address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCPT_COUNT_ZERO", - "weight": 0.0, - "description": "No recipients", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MAILER_1C_8", - "weight": 0.0, - "description": "Sent with 1C:Enterprise 8", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "REPTO_QUOTE_YAHOO", - "weight": 2.0, - "description": "Quoted Reply-To header from Yahoo (seems to be forged)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCVD_COUNT_SEVEN", - "weight": 0.0, - "description": "Message has 7-11 Received headers", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCVD_COUNT_ZERO", - "weight": 0.0, - "description": "Message has no Received headers", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SPOOF_DISPLAY_NAME", - "weight": 8.0, - "description": "Display name is being used to spoof and trick the recipient", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "TO_DN_EQ_ADDR_ALL", - "weight": 0.0, - "description": "All of the recipients have display names that are the same as their address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "CHECK_FROM", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SUBJECT_ENDS_EXCLAIM", - "weight": 0.0, - "description": "Subject ends with an exclamation mark", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_IMS", - "weight": 3.0, - "description": "Forged X-Mailer: Internet Mail Service", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_SENDER", - "weight": 0.300000, - "description": "Sender is forged (different From: header and smtp MAIL FROM: addresses)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCVD_COUNT_ONE", - "weight": 0.0, - "description": "Message has one Received header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "INVALID_RCPT_8BIT", - "weight": 6.0, - "description": "Invalid 8bit character in recipients headers", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_MUA_THEBAT_BOUN", - "weight": 2.0, - "description": "Forged The Bat! MUA headers", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MAIL_RU_MAILER", - "weight": 0.0, - "description": "Sent with Mail.Ru webmail", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HEADER_CC_EMPTY_DELIMITER", - "weight": 1.0, - "description": "Cc header has no delimiter between header name and header value", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "OLD_X_MAILER", - "weight": 2.0, - "description": "X-Mailer header has a very old MUA version", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_GENERIC_RECEIVED4", - "weight": 3.600000, - "description": "Forged generic Received header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FAKE_REPLY", - "weight": 1.0, - "description": "Fake reply", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "STRONGMAIL", - "weight": 6.0, - "description": "Sent via rogue \"strongmail\" MTA", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HAS_X_PRIO_FIVE", - "weight": 0.0, - "description": "Message has X-Priority header set to 5 or higher", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MISSING_MIME_VERSION", - "weight": 2.0, - "description": "MIME-Version header is missing in MIME message", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "CHECK_RCVD", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCVD_DOUBLE_IP_SPAM", - "weight": 2.0, - "description": "Has two Received headers containing bare IP addresses", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HAS_REPLYTO", - "weight": 0.0, - "description": "Has Reply-To header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MIME_MA_MISSING_HTML", - "weight": 1.0, - "description": "MIME multipart/alternative missing text/html part", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "REPLYTO_DN_EQ_FROM_DN", - "weight": 0.0, - "description": "Reply-To display name matches From", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "REPLYTO_DOM_EQ_TO_DOM", - "weight": 0.0, - "description": "Reply-To domain matches the To domain", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "X_PHPOS_FAKE", - "weight": 3.0, - "description": "Fake X-PHP-Originating-Script header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ENVFROM_VERP", - "weight": 0.0, - "description": "Envelope From is a VERP address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FROM_EQ_ENVFROM", - "weight": 0.0, - "description": "From address is the same as the envelope", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HAS_ORG_HEADER", - "weight": 0.0, - "description": "Has Organization header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MISSING_TO", - "weight": 2.0, - "description": "To header is missing", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "BROKEN_HEADERS", - "weight": 10.0, - "description": "Headers structure is likely broken", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FROM_DN_EQ_ADDR", - "weight": 1.0, - "description": "From header display name is the same as the address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FREEMAIL_REPLYTO_NEQ_FROM_DOM", - "weight": 3.0, - "description": "The From and Reply-To addresses in the email are from different freemail services", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RECEIVED_HELO_LOCALHOST", - "weight": 0.0, - "description": "Localhost HELO seen in Received header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_BAD_CTE_7BIT", - "weight": 3.500000, - "description": "Detects bad Content-Transfer-Encoding for text parts", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HEADER_FROM_EMPTY_DELIMITER", - "weight": 1.0, - "description": "From header has no delimiter between header name and header value", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SUBJECT_HAS_QUESTION", - "weight": 0.0, - "description": "Subject contains a question mark", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HAS_X_PRIO_ZERO", - "weight": 0.0, - "description": "Message has X-Priority header set to 0", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "TO_DN_SOME", - "weight": 0.0, - "description": "Some of the recipients have display names", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ONCE_RECEIVED", - "weight": 0.100000, - "description": "One received header in a message", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "INFO_TO_INFO_LU", - "weight": 2.0, - "description": "info@ From/To address with List-Unsubscribe headers", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "REPLYTO_DOM_EQ_FROM_DOM", - "weight": 0.0, - "description": "Reply-To domain matches the From domain", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MIME_MA_MISSING_TEXT", - "weight": 2.0, - "description": "MIME multipart/alternative missing text/plain part", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCPT_COUNT_TWO", - "weight": 0.0, - "description": "Two recipients", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCPT_COUNT_THREE", - "weight": 0.0, - "description": "3-5 recipients", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HAS_X_PRIO", - "weight": 0.0, - "description": "X-Priority check callback rule", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "TO_DN_NONE", - "weight": 0.0, - "description": "None of the recipients have display names", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCVD_COUNT_TWO", - "weight": 0.0, - "description": "Message has two Received headers", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "CTE_CASE", - "weight": 0.500000, - "description": "[78]Bit .vs. [78]bit", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SUBJECT_HAS_EXCLAIM", - "weight": 0.0, - "description": "Subject contains an exclamation mark", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MISSING_XM_UA", - "weight": 0.0, - "description": "Message has neither X-Mailer nor User-Agent header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "X_PHP_FORGED_0X", - "weight": 4.0, - "description": "X-PHP-Originating-Script header appears forged", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "APPLE_IOS_MAILER", - "weight": 0.0, - "description": "Sent with Apple iPhone/iPad Mail", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HAS_LIST_UNSUB", - "weight": -0.010000, - "description": "Has List-Unsubscribe header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ENVFROM_INVALID", - "weight": 2.0, - "description": "Envelope from does not have a valid format", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_GENERIC_RECEIVED3", - "weight": 3.600000, - "description": "Forged generic Received header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_MIXED_CHARSET", - "weight": 5.0, - "description": "Mixed characters in a message", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "INVALID_MSGID", - "weight": 1.700000, - "description": "Message-ID header is incorrect", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "REPLYTO_DOM_NEQ_FROM_DOM", - "weight": 0.0, - "description": "Reply-To domain does not match the From domain", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SUBJECT_ENDS_SPACES", - "weight": 0.500000, - "description": "Subject ends with space characters", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCVD_COUNT_TWELVE", - "weight": 0.0, - "description": "Message has 12 or more Received headers", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FROM_NEQ_DISPLAY_NAME", - "weight": 4.0, - "description": "Display name contains an email address different to the From address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "BROKEN_CONTENT_TYPE", - "weight": 1.500000, - "description": "Message has part with broken content type", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MISSING_DATE", - "weight": 1.0, - "description": "Date header is missing", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_MSGID_YAHOO", - "weight": 2.0, - "description": "Forged Yahoo Message-ID header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "TO_DN_EQ_ADDR_SOME", - "weight": 0.0, - "description": "Some of the recipients have display names that are the same as their address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_RCVD_SPAMBOTS", - "weight": 3.0, - "description": "Spambots signatures in received headers", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_MISSING_CHARSET", - "weight": 0.500000, - "description": "Charset header is missing", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MISSING_MID", - "weight": 2.500000, - "description": "Message-ID header is missing", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HEADER_FORGED_MDN", - "weight": 2.0, - "description": "Read confirmation address is different to return path", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SPOOF_REPLYTO", - "weight": 6.0, - "description": "Reply-To is being used to spoof and trick the recipient to send an off-domain reply", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HEADER_DATE_EMPTY_DELIMITER", - "weight": 1.0, - "description": "Date header has no delimiter between header name and header value", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "TO_MATCH_ENVRCPT_SOME", - "weight": 0.0, - "description": "Some of the recipients match the envelope", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_RECIPIENTS_MAILLIST", - "weight": 0.0, - "description": "Recipients are not the same as RCPT TO: mail command, but a message from a maillist", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MISSING_FROM", - "weight": 2.0, - "description": "Missing From header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCPT_COUNT_SEVEN", - "weight": 0.0, - "description": "7-11 recipients", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "REPLYTO_UNPARSEABLE", - "weight": 1.0, - "description": "Reply-To header could not be parsed", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HAS_X_PRIO_ONE", - "weight": 0.0, - "description": "Message has X-Priority header set to 1", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCPT_COUNT_GT_50", - "weight": 0.0, - "description": "50+ recipients", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCVD_TLS_LAST", - "weight": 0.0, - "description": "Last hop used encrypted transports", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FROM_NAME_HAS_TITLE", - "weight": 1.0, - "description": "From header display name has a title (Mr/Mrs/Dr)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "PREVIOUSLY_DELIVERED", - "weight": 0.0, - "description": "Message either to a list or was forwarded", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCVD_HELO_USER", - "weight": 3.0, - "description": "HELO User spam pattern", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_X_MAILER", - "weight": 4.500000, - "description": "Forged X-Mailer header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_HTTP_URL_IN_FROM", - "weight": 5.0, - "description": "HTTP URL preceded by the start of a line, quote, or whitespace, with normal or URL-encoded colons in From header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "TO_DOM_EQ_FROM_DOM", - "weight": 0.0, - "description": "To domain is the same as the From domain", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCPT_COUNT_TWELVE", - "weight": 0.0, - "description": "12-50 recipients", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_OUTLOOK_TAGS", - "weight": 2.100000, - "description": "Message pretends to be send from Outlook but has 'strange' tags", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FROM_NO_DN", - "weight": 0.0, - "description": "From header does not have a display name", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "INVALID_DATE", - "weight": 1.500000, - "description": "Malformed Date header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_NO_SPACE_IN_FROM", - "weight": 1.0, - "description": "No space in From header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_OUTLOOK_HTML", - "weight": 5.0, - "description": "Forged Outlook HTML signature", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FROM_DISPLAY_CALLBACK", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "REPLYTO_ADDR_EQ_FROM", - "weight": 0.0, - "description": "Reply-To header is identical to SMTP From", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_SENDER_MAILLIST", - "weight": 0.0, - "description": "Sender is not the same as MAIL FROM: envelope, but a message is from a maillist", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "TO_WRAPPED_IN_SPACES", - "weight": 2.0, - "description": "To address is wrapped in spaces inside angle brackets (e.g. display-name < local-part@domain >)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DIRECT_TO_MX", - "weight": 0.0, - "description": "Message has been directly delivered from MUA to local MX", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCVD_COUNT_FIVE", - "weight": 0.0, - "description": "Message has 5-7 Received headers", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_GENERIC_RECEIVED", - "weight": 3.600000, - "description": "Forged generic Received header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SUBJECT_ENDS_QUESTION", - "weight": 1.0, - "description": "Subject ends with a question mark", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_CALLBACK", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_RECIPIENTS", - "weight": 2.0, - "description": "Recipients are not the same as RCPT TO: mail command", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "TRACKER_ID", - "weight": 3.840000, - "description": "Spam string at the end of message to make statistics fault", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FROM_NEQ_ENVFROM", - "weight": 0.0, - "description": "From address is different to the envelope", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "CT_EXTRA_SEMI", - "weight": 1.0, - "description": "Content-Type header ends with a semi-colon", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MAILLIST", - "weight": -0.200000, - "description": "Message seems to be from maillist", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HAS_X_PRIO_TWO", - "weight": 0.0, - "description": "Message has X-Priority header set to 2", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCPT_COUNT_FIVE", - "weight": 0.0, - "description": "5-7 recipients", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MISSING_SUBJECT", - "weight": 2.0, - "description": "Subject header is missing", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "CD_MM_BODY", - "weight": 2.0, - "description": "Content-Description header reads \"Mail message body\", commonly seen in spam", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "YANDEX_RU_MAILER", - "weight": 0.0, - "description": "Sent with Yandex webmail", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "GOOGLE_FORWARDING_MID_MISSING", - "weight": 2.500000, - "description": "Message was missing Message-ID pre-forwarding", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "TO_NEEDS_ENCODING", - "weight": 1.0, - "description": "To header needs encoding", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FROM_NEEDS_ENCODING", - "weight": 1.0, - "description": "From header needs encoding", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SUBJECT_NEEDS_ENCODING", - "weight": 1.0, - "description": "Subject needs encoding", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "REPLYTO_EQ_TO_ADDR", - "weight": 5.0, - "description": "Reply-To is the same as the To address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "REPLYTO_EMAIL_HAS_TITLE", - "weight": 2.0, - "description": "Reply-To header has title", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCPT_COUNT_ONE", - "weight": 0.0, - "description": "One recipient", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "TO_EQ_FROM", - "weight": 0.0, - "description": "To address matches the From address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "CHECK_MIME", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SUSPICIOUS_RECIPS", - "weight": 1.500000, - "description": "Recipients seems to be autogenerated (works if recipients count is more than 5)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FAKE_RECEIVED_mail_ru", - "weight": 4.0, - "description": "Fake HELO mail.ru in Received header from non-mail.ru sender address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HAS_XOIP", - "weight": 0.0, - "description": "Has X-Originating-IP header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "REPLYTO_DOM_NEQ_TO_DOM", - "weight": 0.0, - "description": "Reply-To domain does not match the To domain", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "EMPTY_SUBJECT", - "weight": 1.0, - "description": "Subject header is empty", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "STOX_REPLY_TYPE", - "weight": 1.0, - "description": "Reply-type in Content-Type header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MIME_HEADER_CTYPE_ONLY", - "weight": 2.0, - "description": "Only Content-Type header without other MIME headers", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "BOUNCE", - "weight": -0.100000, - "description": "(Non) Delivery Status Notification", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SORTED_RECIPS", - "weight": 3.500000, - "description": "Recipients list seems to be sorted", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "INVALID_POSTFIX_RECEIVED", - "weight": 3.0, - "description": "Invalid Postfix Received header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ENVFROM_PRVS", - "weight": 0.0, - "description": "Envelope From is a PRVS address that matches the From address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "CHECK_RECEIVED", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MISSING_MIMEOLE", - "weight": 2.0, - "description": "Mime-OLE is needed but absent (e.g. fake Outlook or fake Exchange)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FROM_HAS_DN", - "weight": 0.0, - "description": "From header has a display name", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCVD_NO_TLS_LAST", - "weight": 0.100000, - "description": "Last hop did not use encrypted transports", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "INVALID_FROM_8BIT", - "weight": 6.0, - "description": "Invalid 8bit character in From header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RATWARE_MS_HASH", - "weight": 2.0, - "description": "Forged Exchange messages", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ONCE_RECEIVED_STRICT", - "weight": 4.0, - "description": "One received header with 'bad' patterns inside", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "XM_CASE", - "weight": 0.500000, - "description": "X-mailer .vs. X-Mailer", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DATE_IN_PAST", - "weight": 1.0, - "description": "Message date is in the past", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MULTIPLE_UNIQUE_HEADERS", - "weight": 7.0, - "description": "Repeated unique headers", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HAS_X_PRIO_THREE", - "weight": 0.0, - "description": "Message has X-Priority header set to 3 or 4", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "CHECK_REPLYTO", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_MIXED_CHARSET_URL", - "weight": 7.0, - "description": "Mixed characters in a URL inside message", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MV_CASE", - "weight": 0.500000, - "description": "Mime-Version .vs. MIME-Version", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_UNDISC_RCPT", - "weight": 3.0, - "description": "Recipients are absent or undisclosed", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "APPLE_MAILER", - "weight": 0.0, - "description": "Sent with Apple Mail", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "TO_DN_ALL", - "weight": 0.0, - "description": "All the recipients have display names", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "GOOGLE_FORWARDING_MID_BROKEN", - "weight": 1.700000, - "description": "Message had invalid Message-ID pre-forwarding", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FROM_INVALID", - "weight": 2.0, - "description": "From header does not have a valid format", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DATE_IN_FUTURE", - "weight": 4.0, - "description": "Message date is in the future", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FROM_NAME_EXCESS_SPACE", - "weight": 1.0, - "description": "From header display name contains excess whitespace", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_GENERIC_RECEIVED2", - "weight": 3.600000, - "description": "Forged generic Received header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCVD_COUNT_THREE", - "weight": 0.0, - "description": "Message has 3-5 Received headers", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "REPLYTO_EQ_FROM", - "weight": 0.0, - "description": "Reply-To header is identical to From header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MULTIPLE_FROM", - "weight": 8.0, - "description": "Multiple addresses in From header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HAS_CD_HEADER", - "weight": 0.0, - "description": "Has Content-Description header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCVD_TLS_ALL", - "weight": 0.0, - "description": "All hops used encrypted transports", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "TO_MATCH_ENVRCPT_ALL", - "weight": 0.0, - "description": "All of the recipients match the envelope", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCVD_VIA_SMTP_AUTH", - "weight": 0.0, - "description": "Authenticated hand-off was seen in Received headers", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "TO_DN_RECIPIENTS", - "weight": 2.0, - "description": "To header display name is \"Recipients\"", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MIME_HTML_ONLY", - "weight": 0.200000, - "description": "Message has only an HTML part", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HAS_INTERSPIRE_SIG", - "weight": 1.0, - "description": "Has Interspire fingerprint", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SUBJECT_HAS_CURRENCY", - "weight": 1.0, - "description": "Subject contains currency", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SUBJ_BOUNCE_WORDS", - "weight": 0.0, - "description": "Words/phrases typical for DSN", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HEADER_REPLYTO_EMPTY_DELIMITER", - "weight": 1.0, - "description": "Reply-To header has no delimiter between header name and header value", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HEADER_TO_EMPTY_DELIMITER", - "weight": 1.0, - "description": "To header has no delimiter between header name and header value", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "phishing", - "rules": [ - { - "symbol": "PH_SURBL_MULTI", - "weight": 7.500000, - "description": "A domain in the message is listed in SURBL as phishing", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HACKED_WP_PHISHING", - "weight": 4.500000, - "description": "Phish message sent by hacked Wordpress instance", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URL_REDIRECTOR_NESTED", - "weight": 1.0, - "description": "URL redirector nested limit has been reached" - }, - { - "symbol": "REDIRECTOR_FALSE", - "weight": 0.0, - "description": "Phishing exclusion symbol for known redirectors", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "PHISHED_EXCLUDED", - "weight": 0.0, - "description": "Phished URL found in exclusions list", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "PHISHING", - "weight": 4.0, - "description": "Phished URL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "PHISHED_OPENPHISH", - "weight": 7.0, - "description": "Phished URL found in openphish.com", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "PHISHED_GENERIC_SERVICE", - "weight": 0.0, - "description": "Phished URL found in generic service", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "PHISHED_WHITELISTED", - "weight": 0.0, - "description": "Phishing exclusion symbol for known exceptions", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "PHISHED_PHISHTANK", - "weight": 7.0, - "description": "Phished URL found in phishtank.com", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "excessb64", - "rules": [ - { - "symbol": "FROM_EXCESS_BASE64", - "weight": 1.500000, - "description": "From header is unnecessarily encoded in base64", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "REPLYTO_EXCESS_BASE64", - "weight": 1.500000, - "description": "Reply-To header is unnecessarily encoded in base64", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "TO_EXCESS_BASE64", - "weight": 1.500000, - "description": "To header is unnecessarily encoded in base64", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "CC_EXCESS_BASE64", - "weight": 1.500000, - "description": "Cc header is unnecessarily encoded in base64", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SUBJ_EXCESS_BASE64", - "weight": 1.500000, - "description": "Subject header is unnecessarily encoded in base64", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "forwarding", - "rules": [ - { - "symbol": "FWD_MAILRU", - "weight": 0.0, - "description": "Message was forwarded by Mail.ru", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORWARDED", - "weight": 0.0, - "description": "Message was forwarded", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FWD_GOOGLE", - "weight": 0.0, - "description": "Message was forwarded by Google", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FWD_SIEVE", - "weight": 0.0, - "description": "Message was forwarded using Sieve", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FWD_CPANEL", - "weight": 0.0, - "description": "Message was forwarded using cPanel", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FWD_YANDEX", - "weight": 0.0, - "description": "Message was forwarded by Yandex", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FWD_SRS", - "weight": 0.0, - "description": "Message was forwarded using Sender Rewriting Scheme (SRS)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "url", - "rules": [ - { - "symbol": "HAS_FILE_URL", - "weight": 2.0, - "description": "Contains file:// URL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URL_BAD_UNICODE", - "weight": 3.0, - "description": "URL contains invalid Unicode", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URL_USER_PASSWORD", - "weight": 2.0, - "description": "URL contains user field", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URL_OBFUSCATED_TEXT", - "weight": 5.0, - "description": "Obfuscated URL found in message text", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URL_VERY_LONG", - "weight": 1.500000, - "description": "URL is very long", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URL_HOMOGRAPH_ATTACK", - "weight": 5.0, - "description": "URL uses homograph attack (mixed scripts)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URL_SUSPICIOUS_TLD", - "weight": 3.0, - "description": "URL uses suspicious TLD", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HAS_GOOGLE_REDIR", - "weight": 1.0, - "description": "Has google.com/url or alike Google redirection URL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URI_COUNT_ODD", - "weight": 1.0, - "description": "Odd number of URIs in multipart/alternative message", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URL_ZERO_WIDTH_SPACES", - "weight": 7.0, - "description": "URL contains zero-width spaces", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URL_USER_LONG", - "weight": 3.0, - "description": "URL user field is long (>128 chars)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HAS_GOOGLE_FIREBASE_URL", - "weight": 2.0, - "description": "Contains firebasestorage.googleapis.com URL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HAS_IPFS_GATEWAY_URL", - "weight": 6.0, - "description": "Message contains InterPlanetary File System (IPFS) gateway URL, likely malicious", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URL_RTL_OVERRIDE", - "weight": 6.0, - "description": "URL uses RTL override character", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URL_NUMERIC_PRIVATE_IP", - "weight": 0.500000, - "description": "URL uses private IP range", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URL_BACKSLASH_PATH", - "weight": 2.0, - "description": "URL uses backslashes", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URL_NUMERIC_IP", - "weight": 1.500000, - "description": "URL uses numeric IP address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URL_USER_VERY_LONG", - "weight": 5.0, - "description": "URL user field is very long (>256 chars)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HAS_ONION_URI", - "weight": 0.0, - "description": "Contains .onion hidden service URI", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URL_EXCESSIVE_DOTS", - "weight": 2.0, - "description": "URL has excessive dots in hostname", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URL_SUSPECT_CHECK", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URL_NO_TLD", - "weight": 2.0, - "description": "URL has no TLD", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "OMOGRAPH_URL", - "weight": 5.0, - "description": "URL contains both latin and non-latin characters", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URL_MULTIPLE_AT_SIGNS", - "weight": 3.0, - "description": "URL has multiple @ signs", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URL_NUMERIC_IP_USER", - "weight": 4.0, - "description": "URL uses numeric IP with user field", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HAS_GUC_PROXY_URI", - "weight": 1.0, - "description": "Has googleusercontent.com proxy URL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "rspamdbl", - "rules": [ - { - "symbol": "RSPAMD_URIBL", - "weight": 4.500000, - "description": "Rspamd uribl, bl.rspamd.com", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RSPAMD_EMAILBL", - "weight": 2.500000, - "description": "Rspamd emailbl, bl.rspamd.com", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "blocked", - "rules": [ - { - "symbol": "RECEIVED_SPAMHAUS_BLOCKED_OPENRESOLVER", - "weight": 0.0, - "description": "You are querying Spamhaus from an open resolver, please see https://www.spamhaus.org/returnc/pub/", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SURBL_BLOCKED", - "weight": 0.0, - "description": "SURBL: query blocked by policy/overusage", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SPAMHAUS_BLOCKED", - "weight": 0.0, - "description": "You are exceeding the query limit, please see https://www.spamhaus.org/returnc/vol/", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_BLOCKED_OPENRESOLVER", - "weight": 0.0, - "description": "You are querying Spamhaus from an open resolver, please see https://www.spamhaus.org/returnc/pub/", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RECEIVED_SPAMHAUS_BLOCKED", - "weight": 0.0, - "description": "You are exceeding the query limit, please see https://www.spamhaus.org/returnc/vol/", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DNSWL_BLOCKED", - "weight": 0.0, - "description": "https://www.dnswl.org: Resolver blocked due to excessive queries", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DWL_DNSWL_BLOCKED", - "weight": 0.0, - "description": "https://www.dnswl.org: Resolver blocked due to excessive queries (DWL)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SPAMHAUS_BLOCKED_OPENRESOLVER", - "weight": 0.0, - "description": "You are querying Spamhaus from an open resolver, please see https://www.spamhaus.org/returnc/pub/", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URIBL_BLOCKED", - "weight": 0.0, - "description": "URIBL.com: query refused, likely due to policy/overusage", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_REPUT_BLOCKED", - "weight": 0.0, - "description": "Excessive number of queries to SenderScore RPBL, more info: https://knowledge.validity.com/hc/en-us/articles/20961730681243", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_BLOCKED", - "weight": 0.0, - "description": "Excessive number of queries to SenderScore RPBL, more info: https://knowledge.validity.com/hc/en-us/articles/20961730681243" - }, - { - "symbol": "DBL_BLOCKED", - "weight": 0.0, - "description": "You are exceeding the query limit, please see https://www.spamhaus.org/returnc/vol/", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "blocklistde", - "rules": [ - { - "symbol": "RECEIVED_BLOCKLISTDE", - "weight": 3.0, - "description": "Received address is listed in Blocklist (https://www.blocklist.de/)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_BLOCKLISTDE", - "weight": 4.0, - "description": "From address is listed in Blocklist (https://www.blocklist.de/)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "mime_types", - "rules": [ - { - "symbol": "MIME_DOUBLE_BAD_EXTENSION", - "weight": 3.0, - "description": "Bad extension cloaking", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MIME_TRACE", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MIME_ARCHIVE_IN_ARCHIVE", - "weight": 5.0, - "description": "Archive within another archive", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MIME_UNKNOWN", - "weight": 0.100000, - "description": "Missing or unknown content-type", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ENCRYPTED_PGP", - "weight": -0.500000, - "description": "Message is encrypted with PGP", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MIME_GOOD", - "weight": -0.100000, - "description": "Known content-type", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "BOGUS_ENCRYPTED_AND_TEXT", - "weight": 10.0, - "description": "Bogus mix of encrypted and text/html payloads", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MIME_BAD_EXTENSION", - "weight": 2.0, - "description": "Bad extension", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MIME_EXE_IN_GEN_SPLIT_RAR", - "weight": 5.0, - "description": "EXE file in RAR archive with generic split extension (e.g. .001)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MIME_ENCRYPTED_ARCHIVE", - "weight": 2.0, - "description": "Encrypted archive in a message", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MIME_BAD", - "weight": 1.0, - "description": "Known bad content-type", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SIGNED_SMIME", - "weight": -2.0, - "description": "Message is signed with S/MIME", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MIME_TYPES_CALLBACK", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MIME_BAD_UNICODE", - "weight": 2.0, - "description": "Filename with known obscured unicode characters", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SIGNED_PGP", - "weight": -2.0, - "description": "Message is signed with PGP", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MIME_OBFUSCATED_ARCHIVE", - "weight": 2.0, - "description": "Archive has files with clear obfuscation signs", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ENCRYPTED_SMIME", - "weight": -0.500000, - "description": "Message is encrypted with S/MIME", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MIME_BAD_ATTACHMENT", - "weight": 4.0, - "description": "Invalid attachment mime type", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "antivirus", - "rules": [] - }, - { - "group": "spf", - "rules": [ - { - "symbol": "R_SPF_FAIL", - "weight": 1.0, - "description": "SPF verification failed", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SPF_CHECK", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "WHITELIST_SPF_DKIM", - "weight": -3.0, - "description": "Mail comes from the whitelisted domain and has valid SPF and DKIM policies", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "BLACKLIST_DMARC", - "weight": 6.0, - "description": "Mail comes from the whitelisted domain and has failed DMARC and DKIM policies", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "BLACKLIST_SPF_DKIM", - "weight": 3.0, - "description": "Mail comes from the whitelisted domain and has no valid SPF policy or a bad DKIM signature", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_SPF_PERMFAIL", - "weight": 0.0, - "description": "SPF record is malformed or persistent DNS error", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_SPF_ALLOW", - "weight": -0.200000, - "description": "SPF verification allows sending", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_SPF_SOFTFAIL", - "weight": 0.0, - "description": "SPF verification soft-failed", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_SPF_NEUTRAL", - "weight": 0.0, - "description": "SPF policy is neutral", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_SPF_PLUSALL", - "weight": 4.0, - "description": "SPF record allows to send from any IP", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "WHITELIST_DMARC", - "weight": -7.0, - "description": "Mail comes from the whitelisted domain and has valid DMARC and DKIM policies", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_SPF_DNSFAIL", - "weight": 0.0, - "description": "SPF DNS failure", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_SPF_NA", - "weight": 0.0, - "description": "Missing SPF record", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "BLACKLIST_SPF", - "weight": 1.0, - "description": "Mail comes from the whitelisted domain and has no valid SPF policy", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "WHITELIST_SPF", - "weight": -1.0, - "description": "Mail comes from the whitelisted domain and has a valid SPF policy", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "hfilter", - "rules": [ - { - "symbol": "HFILTER_URL_ONELINE", - "weight": 2.500000, - "description": "One line URL and text in body", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HFILTER_HELO_3", - "weight": 2.0, - "description": "Helo host checks (medium)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HFILTER_HOSTNAME_1", - "weight": 0.500000, - "description": "Hostname checks (very low)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HFILTER_HELO_4", - "weight": 2.500000, - "description": "Helo host checks (hard)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HFILTER_HELO_BAREIP", - "weight": 3.0, - "description": "Helo host is bare ip", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HFILTER_HOSTNAME_4", - "weight": 2.500000, - "description": "Hostname checks (hard)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HFILTER_HELO_1", - "weight": 0.500000, - "description": "Helo host checks (very low)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HFILTER_HELO_5", - "weight": 3.0, - "description": "Helo host checks (very hard)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HFILTER_HELO_NORESOLVE_MX", - "weight": 0.200000, - "description": "MX found in Helo and no resolve", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HFILTER_HOSTNAME_3", - "weight": 2.0, - "description": "Hostname checks (medium)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HFILTER_RCPT_BOUNCEMOREONE", - "weight": 1.500000, - "description": "Message from bounce and over 1 recipient", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HFILTER_FROMHOST_NORES_A_OR_MX", - "weight": 1.500000, - "description": "FROM host no resolve to A or MX", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HFILTER_HELO_2", - "weight": 1.0, - "description": "Helo host checks (low)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HFILTER_HELO_BADIP", - "weight": 4.500000, - "description": "Helo host is very bad ip", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HFILTER_HOSTNAME_2", - "weight": 1.0, - "description": "Hostname checks (low)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HFILTER_HOSTNAME_5", - "weight": 3.0, - "description": "Hostname checks (very hard)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HFILTER_FROM_BOUNCE", - "weight": 0.0, - "description": "Bounce message", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RDNS_DNSFAIL", - "weight": 0.0, - "description": "PTR verification DNS error", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HFILTER_HELO_NOT_FQDN", - "weight": 2.0, - "description": "Helo not FQDN", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HFILTER_HELO_NORES_A_OR_MX", - "weight": 0.300000, - "description": "Helo no resolve to A or MX", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HFILTER_FROMHOST_NORESOLVE_MX", - "weight": 0.500000, - "description": "MX found in FROM host and no resolve", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HFILTER_FROMHOST_NOT_FQDN", - "weight": 3.0, - "description": "FROM host not FQDN", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HFILTER_HOSTNAME_UNKNOWN", - "weight": 2.500000, - "description": "Unknown client hostname (PTR or FCrDNS verification failed)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RDNS_NONE", - "weight": 2.0, - "description": "Cannot resolve reverse DNS for sender's IP", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HFILTER_HELO_IP_A", - "weight": 1.0, - "description": "Helo A IP != hostname IP", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HFILTER_URL_ONLY", - "weight": 2.200000, - "description": "URL only in body", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "spamhaus", - "rules": [ - { - "symbol": "RBL_SPAMHAUS_DROP", - "weight": 7.0, - "description": "From address is listed in Spamhaus DROP", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SPAMHAUS_PBL", - "weight": 2.0, - "description": "From address is listed in Spamhaus PBL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_BLOCKED", - "weight": 0.0, - "description": "You are exceeding the query limit, please see https://www.spamhaus.org/returnc/vol/", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SPAMHAUS_BLOCKED", - "weight": 0.0, - "description": "You are exceeding the query limit, please see https://www.spamhaus.org/returnc/vol/", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_ABUSE_BOTNET", - "weight": 6.500000, - "description": "A domain in the message is listed in Spamhaus DBL as abused legit botnet C&C", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SPAMHAUS_BLOCKED_OPENRESOLVER", - "weight": 0.0, - "description": "You are querying Spamhaus from an open resolver, please see https://www.spamhaus.org/returnc/pub/", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_PROHIBIT", - "weight": 0.0, - "description": "DBL uribl IP queries prohibited!", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_ABUSE", - "weight": 5.0, - "description": "A domain in the message is listed in Spamhaus DBL as abused legit spam", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SPAMHAUS_ZEN_URIBL", - "weight": 0.0, - "description": "Unrecognised result from Spamhaus ZEN URIBL" - }, - { - "symbol": "RBL_SPAMHAUS", - "weight": 0.0, - "description": "Unrecognised result from Spamhaus ZEN", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RECEIVED_SPAMHAUS_BLOCKED", - "weight": 0.0, - "description": "You are exceeding the query limit, please see https://www.spamhaus.org/returnc/vol/", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_BOTNET", - "weight": 7.500000, - "description": "A domain in the message is listed in Spamhaus DBL as botnet C&C", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RECEIVED_SPAMHAUS_PBL", - "weight": 0.0, - "description": "Received address is listed in Spamhaus PBL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URIBL_SBL", - "weight": 6.500000, - "description": "A domain in the message body resolves to an IP listed in Spamhaus SBL" - }, - { - "symbol": "RBL_SPAMHAUS_SBL", - "weight": 4.0, - "description": "From address is listed in Spamhaus SBL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_BLOCKED_OPENRESOLVER", - "weight": 0.0, - "description": "You are querying Spamhaus from an open resolver, please see https://www.spamhaus.org/returnc/pub/", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RECEIVED_SPAMHAUS_SBL", - "weight": 3.0, - "description": "Received address is listed in Spamhaus SBL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_ABUSE_REDIR", - "weight": 5.0, - "description": "A domain in the message is listed in Spamhaus DBL as spammed redirector domain", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SPAMHAUS_CSS", - "weight": 2.0, - "description": "From address is listed in Spamhaus CSS", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_ABUSE_PHISH", - "weight": 6.500000, - "description": "A domain in the message is listed in Spamhaus DBL as abused legit phish", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RECEIVED_SPAMHAUS_XBL", - "weight": 1.0, - "description": "Received address is listed in Spamhaus XBL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_SPAM", - "weight": 6.500000, - "description": "A domain in the message is listed in Spamhaus DBL as spam", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URIBL_PBL", - "weight": 0.010000, - "description": "A domain in the message body resolves to an IP listed in Spamhaus PBL" - }, - { - "symbol": "URIBL_DROP", - "weight": 5.0, - "description": "A domain in the message body resolves to an IP listed in Spamhaus DROP" - }, - { - "symbol": "RECEIVED_SPAMHAUS_CSS", - "weight": 1.0, - "description": "Received address is listed in Spamhaus CSS", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_PHISH", - "weight": 7.500000, - "description": "A domain in the message is listed in Spamhaus DBL as phishing", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_ABUSE_MALWARE", - "weight": 6.500000, - "description": "A domain in the message is listed in Spamhaus DBL as abused legit malware", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SPAMHAUS_XBL", - "weight": 4.0, - "description": "From address is listed in Spamhaus XBL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL", - "weight": 0.0, - "description": "Unrecognised result from Spamhaus DBL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_MALWARE", - "weight": 7.500000, - "description": "A domain in the message is listed in Spamhaus DBL as malware", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RECEIVED_SPAMHAUS_BLOCKED_OPENRESOLVER", - "weight": 0.0, - "description": "You are querying Spamhaus from an open resolver, please see https://www.spamhaus.org/returnc/pub/", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URIBL_XBL", - "weight": 3.0, - "description": "A domain in the message body resolves to an IP listed in Spamhaus XBL" - }, - { - "symbol": "URIBL_SBL_CSS", - "weight": 5.0, - "description": "A domain in the message body resolves to an IP listed in Spamhaus CSS" - }, - { - "symbol": "RECEIVED_SPAMHAUS_DROP", - "weight": 6.0, - "description": "Received address is listed in Spamhaus DROP", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "ebl", - "rules": [ - { - "symbol": "MSBL_EBL", - "weight": 7.500000, - "description": "MSBL emailbl (https://www.msbl.org/)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MSBL_EBL_GREY", - "weight": 0.500000, - "description": "MSBL emailbl grey list (https://www.msbl.org/)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "surblorg", - "rules": [ - { - "symbol": "CRACKED_SURBL", - "weight": 5.0, - "description": "A domain in the message is listed in SURBL as cracked", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SURBL_BLOCKED", - "weight": 0.0, - "description": "SURBL: query blocked by policy/overusage", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "PH_SURBL_MULTI", - "weight": 7.500000, - "description": "A domain in the message is listed in SURBL as phishing", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ABUSE_SURBL", - "weight": 5.0, - "description": "A domain in the message is listed in SURBL as abused", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "CT_SURBL", - "weight": 0.0, - "description": "A domain in the message is listed in SURBL as a clicktracker", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MW_SURBL_MULTI", - "weight": 7.500000, - "description": "A domain in the message is listed in SURBL as malware", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DM_SURBL", - "weight": 0.0, - "description": "A domain in the message is listed in SURBL as belonging to a disposable email service", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "uribl", - "rules": [ - { - "symbol": "URIBL_GREY", - "weight": 2.500000, - "description": "A domain in the message is listed in URIBL.com grey", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URIBL_MULTI", - "weight": 0.0, - "description": "Unrecognised result from URIBL.com", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URIBL_BLOCKED", - "weight": 0.0, - "description": "URIBL.com: query refused, likely due to policy/overusage", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URIBL_BLACK", - "weight": 7.500000, - "description": "A domain in the message is listed in URIBL.com black", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URIBL_RED", - "weight": 0.500000, - "description": "A domain in the message is listed in URIBL.com red", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "external_services", - "rules": [] - }, - { - "group": "experimental", - "rules": [ - { - "symbol": "XM_UA_NO_VERSION", - "weight": 0.010000, - "description": "X-Mailer/User-Agent header has no version number", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "composite", - "rules": [ - { - "symbol": "SUSPICIOUS_AUTH_ORIGIN", - "weight": 0.0, - "description": "Message authenticated, but from a suspicios origin (potentially an injector)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_RECIPIENTS_FORWARDING", - "weight": 0.0, - "description": "FORGED_RECIPIENTS & g:forwarding", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "UNDISC_RCPTS_BULK", - "weight": 3.0, - "description": "Missing or undisclosed recipients with a bulk signature", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SUSPICIOUS_URL_IN_SUSPICIOUS_MESSAGE", - "weight": 1.0, - "description": "Message contains redirector, anonymous or IPFS gateway URL and is marked by fuzzy/bayes/SURBL/RBL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCVD_UNAUTH_PBL", - "weight": 2.0, - "description": "Relayed through Spamhaus PBL IP without sufficient authentication (possibly indicating an open relay)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "APPLE_MAILER_COMMON", - "weight": 0.0, - "description": "Message was sent by 'Apple Mail' and has common symbols in place", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_SENDER_MAILLIST", - "weight": 0.0, - "description": "Sender is not the same as MAIL FROM: envelope, but a message is from a maillist", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "PHISH_EMOTION", - "weight": 1.0, - "description": "Phish message with subject trying to address users emotion", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DMARC_POLICY_ALLOW_WITH_FAILURES", - "weight": -0.500000, - "description": "DMARC permit policy with DKIM/SPF failure", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "AUTH_NA_OR_FAIL", - "weight": 1.0, - "description": "No authenticating method SPF/DKIM/DMARC/ARC was successful", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "REDIRECTOR_URL_ONLY", - "weight": 1.0, - "description": "Message only contains a redirector URL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_RECIPIENTS_MAILLIST", - "weight": 0.0, - "description": "Recipients are not the same as RCPT TO: mail command, but a message from a maillist", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_SENDER_VERP_SRS", - "weight": 0.0, - "description": "FORGED_SENDER & (ENVFROM_PRVS | ENVFROM_VERP)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HAS_ANON_DOMAIN", - "weight": 0.100000, - "description": "Contains one or more domains trying to disguise owner/destination", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "BROKEN_HEADERS_MAILLIST", - "weight": 0.0, - "description": "Negate BROKEN_HEADERS when message comes via some mailing list", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "AUTOGEN_PHP_SPAMMY", - "weight": 1.0, - "description": "Message was generated by PHP script and contains some spam indicators", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "APPLE_IOS_MAILER_COMMON", - "weight": 0.0, - "description": "Message was sent by 'Apple iOS Mail' and has common symbols in place", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "IP_SCORE_FREEMAIL", - "weight": 0.0, - "description": "Negate IP_SCORE when message comes from FreeMail", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "VIOLATED_DIRECT_SPF", - "weight": 3.500000, - "description": "Has no Received (or no trusted received relays) and SPF policy fails or soft fails", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "AUTH_NA", - "weight": 1.0, - "description": "Authenticating message via SPF/DKIM/DMARC/ARC not available", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FREEMAIL_REPLYTO_NEQ_FROM", - "weight": 2.0, - "description": "Reply-To is a Freemail address and it not match From header or SMTP From, also From is not another Freemail", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MIME_BAD_EXT_IN_OBFUSCATED_ARCHIVE", - "weight": 8.0, - "description": "Attachment with bad extension and archive that has filename with clear obfuscation signs", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "BAD_REP_POLICIES", - "weight": 0.100000, - "description": "Contains valid policies but are also marked by fuzzy/bayes/SURBL/RBL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MISSING_MID_ALLOWED", - "weight": 0.0, - "description": "MISSING_MID_ALLOWED", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_MUA_MAILLIST", - "weight": 0.0, - "description": "Avoid false positives for FORGED_MUA_* in maillist", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SPF_FAIL_FORWARDING", - "weight": 0.0, - "description": "g:forwarding & (R_SPF_SOFTFAIL | R_SPF_FAIL)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "INVALID_MSGID_ALLOWED", - "weight": 0.0, - "description": "INVALID_MSGID_ALLOWED", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCVD_DKIM_ARC_DNSWL_HI", - "weight": -1.0, - "description": "Sufficiently DKIM/ARC signed and received from IP with high trust at DNSWL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_SENDER_FORWARDING", - "weight": 0.0, - "description": "Forged sender, but message is forwarded", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MIME_BAD_EXT_WITH_BAD_UNICODE", - "weight": 8.0, - "description": "Attachment with bad extension and filename that has known obscured unicode characters", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCVD_DKIM_ARC_DNSWL_MED", - "weight": -0.500000, - "description": "Sufficiently DKIM/ARC signed and received from IP with medium trust at DNSWL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DKIM_MIXED", - "weight": 0.0, - "description": "-R_DKIM_ALLOW & (R_DKIM_TEMPFAIL | R_DKIM_PERMFAIL | R_DKIM_REJECT)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "BOUNCE_NO_AUTH", - "weight": 1.0, - "description": "(AUTH_NA | AUTH_NA_OR_FAIL) & (BOUNCE | SUBJ_BOUNCE_WORDS)", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "mid", - "rules": [ - { - "symbol": "MID_END_EQ_FROM_USER_PART", - "weight": 4.0, - "description": "Message-ID RHS (after @) and MIME from local part are the same", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "CHECK_MID", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "KNOWN_MID", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "KNOWN_NO_MID", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "KNOWN_MID_CALLBACK", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "fuzzy", - "rules": [ - { - "symbol": "FUZZY_DENIED", - "weight": 12.0, - "description": "Denied fuzzy hash, bl.rspamd.com", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FUZZY_PROB", - "weight": 5.0, - "description": "Probable fuzzy hash, bl.rspamd.com", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FUZZY_ENCRYPTION_REQUIRED", - "weight": 0.0, - "description": "Fuzzy encryption is required by a server", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FUZZY_WHITE", - "weight": -2.100000, - "description": "Whitelisted fuzzy hash, bl.rspamd.com", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FUZZY_FORBIDDEN", - "weight": 0.0, - "description": "Fuzzy access denied", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FUZZY_RATELIMITED", - "weight": 0.0, - "description": "Fuzzy rate limit is reached", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FUZZY_UNKNOWN", - "weight": 5.0, - "description": "Generic fuzzy hash match, bl.rspamd.com", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FUZZY_CALLBACK", - "weight": 0.0, - "description": "Fuzzy check callback", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "senderscore", - "rules": [ - { - "symbol": "RBL_SENDERSCORE_NA", - "weight": 0.0, - "description": "From address is listed in SenderScore RPBL - noauth" - }, - { - "symbol": "RBL_SENDERSCORE_REPUT_2", - "weight": 3.0, - "description": "SenderScore Reputation: Bad (20-29).", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_SUS_ATT_NA", - "weight": 1.0, - "description": "From address is listed in SenderScore RPBL - suspect_attachments+noauth" - }, - { - "symbol": "RBL_SENDERSCORE_SCORE", - "weight": 2.0, - "description": "From address is listed in SenderScore RPBL - sender_score" - }, - { - "symbol": "RBL_SENDERSCORE_REPUT_9", - "weight": -1.0, - "description": "SenderScore Reputation: Good (90-100).", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_REPUT_4", - "weight": 2.0, - "description": "SenderScore Reputation: Bad (40-49).", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_REPUT_1", - "weight": 3.500000, - "description": "SenderScore Reputation: Bad (10-19).", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_REPUT_UNKNOWN", - "weight": 0.0, - "description": "Unrecognized result from SenderScore Reputation list.", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_SCORE_NA", - "weight": 2.0, - "description": "From address is listed in SenderScore RPBL - sender_score+noauth" - }, - { - "symbol": "RBL_SENDERSCORE_BLOCKED", - "weight": 0.0, - "description": "Excessive number of queries to SenderScore RPBL, more info: https://knowledge.validity.com/hc/en-us/articles/20961730681243" - }, - { - "symbol": "RBL_SENDERSCORE_REPUT_8", - "weight": 0.0, - "description": "SenderScore Reputation: Neutral (80-89).", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_SCORE_PRST_NA", - "weight": 4.0, - "description": "From address is listed in SenderScore RPBL - sender_score+pristine+noauth" - }, - { - "symbol": "RBL_SENDERSCORE_PRST_NA", - "weight": 2.0, - "description": "From address is listed in SenderScore RPBL - pristine+noauth" - }, - { - "symbol": "RBL_SENDERSCORE_PRST_NA_BOT", - "weight": 3.0, - "description": "From address is listed in SenderScore RPBL - pristine+noauth+botnet" - }, - { - "symbol": "RBL_SENDERSCORE_PRST_BOT", - "weight": 3.0, - "description": "From address is listed in SenderScore RPBL - pristine+botnet" - }, - { - "symbol": "RBL_SENDERSCORE_SCORE_SUS_ATT_NA", - "weight": 3.0, - "description": "From address is listed in SenderScore RPBL - sender_score+suspect_attachments+noauth" - }, - { - "symbol": "RBL_SENDERSCORE_REPUT_6", - "weight": 1.0, - "description": "SenderScore Reputation: Bad (60-69).", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_PRST", - "weight": 2.0, - "description": "From address is listed in SenderScore RPBL - pristine" - }, - { - "symbol": "RBL_SENDERSCORE_REPUT_0", - "weight": 4.0, - "description": "SenderScore Reputation: Very Bad (0-9).", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_SUS_ATT", - "weight": 1.0, - "description": "From address is listed in SenderScore RPBL - suspect_attachments" - }, - { - "symbol": "RBL_SENDERSCORE_SCORE_PRST", - "weight": 4.0, - "description": "From address is listed in SenderScore RPBL - sender_score+pristine" - }, - { - "symbol": "RBL_SENDERSCORE_REPUT_3", - "weight": 2.500000, - "description": "SenderScore Reputation: Bad (30-39).", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_REPUT_5", - "weight": 1.500000, - "description": "SenderScore Reputation: Bad (50-59).", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_SUS_ATT_PRST_NA", - "weight": 3.0, - "description": "From address is listed in SenderScore RPBL - suspect_attachments+pristine+noauth" - }, - { - "symbol": "RBL_SENDERSCORE_NA_BOT", - "weight": 1.0, - "description": "From address is listed in SenderScore RPBL - noauth+botnet" - }, - { - "symbol": "RBL_SENDERSCORE_REPUT_7", - "weight": 0.500000, - "description": "SenderScore Reputation: Bad (70-79).", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_SUS_ATT_NA_BOT", - "weight": 1.500000, - "description": "From address is listed in SenderScore RPBL - suspect_attachments+noauth+botnet" - }, - { - "symbol": "RBL_SENDERSCORE_SUS_ATT_PRST_NA_BOT", - "weight": 3.500000, - "description": "From address is listed in SenderScore RPBL - suspect_attachments+pristine+noauth+botnet" - }, - { - "symbol": "RBL_SENDERSCORE_BOT", - "weight": 2.0, - "description": "From address is listed in SenderScore RPBL - botnet" - }, - { - "symbol": "RBL_SENDERSCORE_REPUT_BLOCKED", - "weight": 0.0, - "description": "Excessive number of queries to SenderScore RPBL, more info: https://knowledge.validity.com/hc/en-us/articles/20961730681243", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "aliases", - "rules": [ - { - "symbol": "TAGGED_RCPT", - "weight": 0.0, - "description": "Recipient has plus-tags", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "TAGGED_FROM", - "weight": 0.0, - "description": "From address has plus-tags", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "INTERNAL_MAIL", - "weight": 0.0, - "description": "Mail from local to local domain", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ALIASES_CHECK", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "LOCAL_INBOUND", - "weight": 0.0, - "description": "Mail from external to local domain", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ALIAS_RESOLVED", - "weight": 0.0, - "description": "Address was resolved through aliases", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "LOCAL_OUTBOUND", - "weight": 0.0, - "description": "Mail from local to external domain", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "malware", - "rules": [ - { - "symbol": "EXE_ARCHIVE_CLICKBAIT_FILENAME", - "weight": 9.0, - "description": "exe file in archive with clickbait filename", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "EXE_ARCHIVE_CLICKBAIT_SUBJECT", - "weight": 9.0, - "description": "exe file in archive with clickbait subject", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MISIDENTIFIED_RAR", - "weight": 4.0, - "description": "rar with wrong extension", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "EXE_IN_ARCHIVE", - "weight": 1.500000, - "description": "exe file in archive", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "EXE_IN_MISIDENTIFIED_RAR", - "weight": 5.0, - "description": "rar with wrong extension containing exe file", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SINGLE_FILE_ARCHIVE_WITH_EXE", - "weight": 5.0, - "description": "single file container bearing executable", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "mailspike", - "rules": [ - { - "symbol": "MAILSPIKE", - "weight": 0.0, - "description": "Unrecognised result from Mailspike", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_MAILSPIKE_BAD", - "weight": 1.0, - "description": "From address is listed in Mailspike RBL - bad reputation", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_MAILSPIKE_VERYBAD", - "weight": 1.500000, - "description": "From address is listed in Mailspike RBL - very bad reputation", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RWL_MAILSPIKE_GOOD", - "weight": -0.100000, - "description": "From address is listed in Mailspike RWL - good reputation", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RWL_MAILSPIKE_VERYGOOD", - "weight": -0.200000, - "description": "From address is listed in Mailspike RWL - very good reputation", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RWL_MAILSPIKE_POSSIBLE", - "weight": 0.0, - "description": "From address is listed in Mailspike RWL - possibly legit", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RWL_MAILSPIKE_EXCELLENT", - "weight": -0.400000, - "description": "From address is listed in Mailspike RWL - excellent reputation", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RWL_MAILSPIKE_NEUTRAL", - "weight": 0.0, - "description": "Neutral result from Mailspike", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_MAILSPIKE_WORST", - "weight": 2.0, - "description": "From address is listed in Mailspike RBL - worst possible reputation", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "compromised_hosts", - "rules": [ - { - "symbol": "URI_HIDDEN_PATH", - "weight": 1.0, - "description": "Message contains URI with a hidden path", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "XAW_SERVICE_ACCT", - "weight": 1.0, - "description": "Message originally from a service account", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HIDDEN_SOURCE_OBJ", - "weight": 2.0, - "description": "UNIX hidden file/directory in path", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HAS_PHPMAILER_SIG", - "weight": 0.0, - "description": "PHPMailer signature", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "WWW_DOT_DOMAIN", - "weight": 0.500000, - "description": "From/Sender/Reply-To or Envelope is @www.domain.com", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HAS_X_SOURCE", - "weight": 0.0, - "description": "Has X-Source headers", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HACKED_WP_PHISHING", - "weight": 4.500000, - "description": "Phish message sent by hacked Wordpress instance", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HAS_XAW", - "weight": 0.0, - "description": "Has X-Authentication-Warning header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HAS_X_PHP_SCRIPT", - "weight": 0.0, - "description": "Has X-PHP-Script header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "PHP_SCRIPT_ROOT", - "weight": 1.0, - "description": "PHP Script executed by root UID", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "PHP_XPS_PATTERN", - "weight": 0.0, - "description": "Message contains X-PHP-Script pattern", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HAS_X_AS", - "weight": 0.0, - "description": "Has X-Authenticated-Sender header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "COMPROMISED_ACCT_BULK", - "weight": 3.0, - "description": "Likely to be from a compromised account", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "X_PHP_EVAL", - "weight": 4.0, - "description": "Message sent using eval'd PHP", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HAS_X_POS", - "weight": 0.0, - "description": "Has X-PHP-Originating-Script header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HAS_WP_URI", - "weight": 0.0, - "description": "Contains WordPress URIs", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ABUSE_FROM_INJECTOR", - "weight": 2.0, - "description": "Message is sent from a suspicios origin and showing signs of abuse, likely spam injected in compromised account", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HAS_X_GMSV", - "weight": 0.0, - "description": "Has X-Get-Message-Sender-Via: header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FROM_SERVICE_ACCT", - "weight": 1.0, - "description": "Sender/From/Reply-To is a service account", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ENVFROM_SERVICE_ACCT", - "weight": 1.0, - "description": "Envelope from is a service account", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HAS_X_ANTIABUSE", - "weight": 0.0, - "description": "Has X-AntiAbuse headers", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "WP_COMPROMISED", - "weight": 0.0, - "description": "URL that is pointing to a compromised WordPress installation", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MID_RHS_WWW", - "weight": 0.500000, - "description": "Message-ID from www host", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "html", - "rules": [ - { - "symbol": "ZERO_FONT", - "weight": 1.0, - "description": "Zero sized font used", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HTML_SHORT_LINK_IMG_1", - "weight": 2.0, - "description": "Short HTML part (0..1K) with a link to an image", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_WHITE_ON_WHITE", - "weight": 4.0, - "description": "Message contains low contrast text", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HTML_SHORT_LINK_IMG_2", - "weight": 1.0, - "description": "Short HTML part (1K..1.5K) with a link to an image", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HTML_VISIBLE_CHECKS", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HTML_SHORT_LINK_IMG_3", - "weight": 0.500000, - "description": "Short HTML part (1.5K..2K) with a link to an image", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HAS_DATA_URI", - "weight": 0.0, - "description": "Has Data URI encoding", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HTTP_TO_IP", - "weight": 1.0, - "description": "HTML anchor points to an IP address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_EMPTY_IMAGE", - "weight": 2.0, - "description": "Message contains empty parts and image", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MANY_INVISIBLE_PARTS", - "weight": 1.0, - "description": "Many parts are visually hidden", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_SUSPICIOUS_IMAGES", - "weight": 5.0, - "description": "Message has high image to text ratio", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HTTP_TO_HTTPS", - "weight": 0.500000, - "description": "The anchor text contains a distinct scheme compared to the target URL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "EXT_CSS", - "weight": 1.0, - "description": "Message contains external CSS reference", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DATA_URI_OBFU", - "weight": 2.0, - "description": "Uses Data URI encoding to obfuscate plain or HTML in base64", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "HTML_META_REFRESH_URL", - "weight": 5.0, - "description": "Has HTML Meta refresh URL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "subject", - "rules": [ - { - "symbol": "SUBJ_ALL_CAPS", - "weight": 3.0, - "description": "Subject contains mostly capital letters", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "LONG_SUBJ", - "weight": 3.0, - "description": "Subject is very long", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URL_IN_SUBJECT", - "weight": 4.0, - "description": "Subject contains URL", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "ungrouped", - "rules": [ - { - "symbol": "ARC_SIGNED", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ASN", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DKIM_SIGNED", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "BLOCKLISTDE_FAIL", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DWL_DNSWL_FAIL", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MSBL_EBL_FAIL", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MAILSPIKE_FAIL", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SPAMHAUS_FAIL", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SEM_URIBL_FRESH15_UNKNOWN_FAIL", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SPF_CHECK", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SURBL_HASHBL_FAIL", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SEM_FAIL", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RCVD_IN_DNSWL_FAIL", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SINGLE_SHORT_PART", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SURBL_MULTI_FAIL", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "UDF_COMPRESSION_500PLUS", - "weight": 9.0, - "description": "very well compressed img file in archive", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "ASN_FAIL", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_VIRUSFREE_UNKNOWN_FAIL", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SENDERSCORE_REPUT_UNKNOWN_FAIL", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RSPAMD_EMAILBL_FAIL", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "URIBL_MULTI_FAIL", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "DBL_FAIL", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RSPAMD_URIBL_FAIL", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "RBL_SEM_IPV6_FAIL", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SEM_URIBL_UNKNOWN_FAIL", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "mua", - "rules": [ - { - "symbol": "FORGED_MUA_THEBAT_MSGID_UNKNOWN", - "weight": 3.0, - "description": "Message pretends to be send from The Bat! but has forged Message-ID", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_MUA_KMAIL_MSGID_UNKNOWN", - "weight": 2.500000, - "description": "Message pretends to be send from KMail but has forged Message-ID", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_MUA_OPERA_MSGID", - "weight": 4.0, - "description": "Message pretends to be send from Opera Mail but has forged Message-ID", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_MUA_SEAMONKEY_MSGID", - "weight": 4.0, - "description": "Forged mail pretending to be from Mozilla Seamonkey but has forged Message-ID", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_MUA_SEAMONKEY_MSGID_UNKNOWN", - "weight": 2.500000, - "description": "Forged mail pretending to be from Mozilla Seamonkey but has forged Message-ID", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_MUA_OUTLOOK", - "weight": 3.0, - "description": "Forged Outlook MUA", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SUSPICIOUS_BOUNDARY2", - "weight": 4.0, - "description": "Suspicious boundary in Content-Type header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_MUA_THEBAT_MSGID", - "weight": 4.0, - "description": "Message pretends to be send from The Bat! but has forged Message-ID", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SUSPICIOUS_BOUNDARY3", - "weight": 3.0, - "description": "Suspicious boundary in Content-Type header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SUSPICIOUS_BOUNDARY4", - "weight": 4.0, - "description": "Suspicious boundary in Content-Type header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SUSPICIOUS_BOUNDARY", - "weight": 5.0, - "description": "Suspicious boundary in Content-Type header", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_MUA_POSTBOX_MSGID_UNKNOWN", - "weight": 2.500000, - "description": "Forged mail pretending to be from Postbox but has forged Message-ID", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_MUA_MOZILLA_MAIL_MSGID", - "weight": 4.0, - "description": "Message pretends to be send from Mozilla Mail but has forged Message-ID", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_MUA_THUNDERBIRD_MSGID_UNKNOWN", - "weight": 2.500000, - "description": "Forged mail pretending to be from Mozilla Thunderbird but has forged Message-ID", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_MUA_MAILLIST", - "weight": 0.0, - "description": "Avoid false positives for FORGED_MUA_* in maillist", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_MUA_THUNDERBIRD_MSGID", - "weight": 4.0, - "description": "Forged mail pretending to be from Mozilla Thunderbird but has forged Message-ID", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_MUA_MOZILLA_MAIL_MSGID_UNKNOWN", - "weight": 2.500000, - "description": "Message pretends to be send from Mozilla Mail but has forged Message-ID", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FORGED_MUA_POSTBOX_MSGID", - "weight": 4.0, - "description": "Forged mail pretending to be from Postbox but has forged Message-ID", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "whitelist", - "rules": [ - { - "symbol": "WHITELIST_DKIM", - "weight": -1.0, - "description": "Mail comes from the whitelisted domain and has a valid DKIM signature", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "WHITELIST_SPF_DKIM", - "weight": -3.0, - "description": "Mail comes from the whitelisted domain and has valid SPF and DKIM policies", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "BLACKLIST_DMARC", - "weight": 6.0, - "description": "Mail comes from the whitelisted domain and has failed DMARC and DKIM policies", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "WHITELIST_DMARC", - "weight": -7.0, - "description": "Mail comes from the whitelisted domain and has valid DMARC and DKIM policies", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "BLACKLIST_SPF_DKIM", - "weight": 3.0, - "description": "Mail comes from the whitelisted domain and has no valid SPF policy or a bad DKIM signature", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "BLACKLIST_DKIM", - "weight": 2.0, - "description": "Mail comes from the whitelisted domain and has non-valid DKIM signature", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "WHITELIST_SPF", - "weight": -1.0, - "description": "Mail comes from the whitelisted domain and has a valid SPF policy", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "BLACKLIST_SPF", - "weight": 1.0, - "description": "Mail comes from the whitelisted domain and has no valid SPF policy", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "blankspam", - "rules": [ - { - "symbol": "COMPLETELY_EMPTY", - "weight": 15.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SHORT_PART_BAD_HEADERS", - "weight": 7.0, - "description": "MISSING_ESSENTIAL_HEADERS & SINGLE_SHORT_PART", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MISSING_ESSENTIAL_HEADERS", - "weight": 7.0, - "description": "Common headers were entirely absent", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "content", - "rules": [ - { - "symbol": "PDF_TIMEOUT", - "weight": 0.0, - "description": "There is a PDF in the message that caused timeout in processing", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "PDF_LONG_TRAILER", - "weight": 0.200000, - "description": "There is an PDF with a long trailer in the message", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "PDF_JAVASCRIPT", - "weight": 0.100000, - "description": "There is an PDF with JavaScript in the message", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "PDF_MANY_OBJECTS", - "weight": 0.0, - "description": "There is a PDF with too many objects in the message", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "PDF_ENCRYPTED", - "weight": 0.300000, - "description": "There is an encrypted PDF in the message", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "PDF_SUSPICIOUS", - "weight": 4.500000, - "description": "There is an PDF with suspicious properties in the message", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "Message ID", - "rules": [ - { - "symbol": "MID_CONTAINS_TO", - "weight": 1.0, - "description": "Message-ID contains To address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MID_MISSING_BRACKETS", - "weight": 0.500000, - "description": "Message-ID is missing <>'s", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MID_RHS_MATCH_TO", - "weight": 1.0, - "description": "Message-ID RHS matches To domain", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MID_RHS_NOT_FQDN", - "weight": 0.500000, - "description": "Message-ID RHS is not a fully-qualified domain name", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MID_RHS_MATCH_FROM", - "weight": 0.0, - "description": "Message-ID RHS matches From domain", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MID_CONTAINS_FROM", - "weight": 1.0, - "description": "Message-ID contains From address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MID_BARE_IP", - "weight": 2.0, - "description": "Message-ID RHS is a bare IP address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MID_RHS_IP_LITERAL", - "weight": 0.500000, - "description": "Message-ID RHS is an IP-literal", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "MID_RHS_MATCH_FROMTLD", - "weight": 0.0, - "description": "Message-ID RHS matches From domain tld", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "headers,mime", - "rules": [ - { - "symbol": "CHECK_TO_CC", - "weight": 0.0, - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "scams", - "rules": [ - { - "symbol": "LEAKED_PASSWORD_SCAM_RE", - "weight": 0.0, - "description": "Contains BTC wallet address and malicious regexps", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "FREEMAIL_AFF", - "weight": 4.0, - "description": "Message exhibits strong characteristics of advance fee fraud (AFF a/k/a '419' spam) involving freemail addresses", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "INTRODUCTION", - "weight": 2.0, - "description": "Sender introduces themselves", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "SUSPICIOUS_MDN", - "weight": 2.0, - "description": "Message delivery notification should go to freemail or disposable e-mail, but message was not sent from a freemail address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "BITCOIN_ADDR", - "weight": 0.0, - "description": "Message has a valid bitcoin wallet address", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "LEAKED_PASSWORD_SCAM", - "weight": 7.0, - "description": "Contains BTC wallet address and scam patterns", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - }, - { - "group": "body", - "rules": [ - { - "symbol": "HAS_ATTACHMENT", - "weight": 0.0, - "description": "Message contains attachments", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - }, - { - "symbol": "R_PARTS_DIFFER", - "weight": 1.0, - "description": "Text and HTML parts differ", - "frequency": 0.0, - "frequency_stddev": 0.0, - "time": 0.0 - } - ] - } -] diff --git a/pkg/analyzer/rspamd.go b/pkg/analyzer/rspamd.go deleted file mode 100644 index 9780f17..0000000 --- a/pkg/analyzer/rspamd.go +++ /dev/null @@ -1,174 +0,0 @@ -// This file is part of the happyDeliver (R) project. -// Copyright (c) 2026 happyDomain -// Authors: Pierre-Olivier Mercier, et al. -// -// This program is offered under a commercial and under the AGPL license. -// For commercial licensing, contact us at . -// -// For AGPL licensing: -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package analyzer - -import ( - "math" - "regexp" - "strconv" - "strings" - - "git.happydns.org/happyDeliver/internal/api" -) - -// Default rspamd action thresholds (rspamd built-in defaults) -const ( - rspamdDefaultRejectThreshold float32 = 15 - rspamdDefaultAddHeaderThreshold float32 = 6 -) - -// RspamdAnalyzer analyzes rspamd results from email headers -type RspamdAnalyzer struct { - symbols map[string]string -} - -// NewRspamdAnalyzer creates a new rspamd analyzer with optional symbol descriptions -func NewRspamdAnalyzer(symbols map[string]string) *RspamdAnalyzer { - return &RspamdAnalyzer{symbols: symbols} -} - -// AnalyzeRspamd extracts and analyzes rspamd results from email headers -func (a *RspamdAnalyzer) AnalyzeRspamd(email *EmailMessage) *api.RspamdResult { - headers := email.GetRspamdHeaders() - if len(headers) == 0 { - return nil - } - - // Require at least X-Spamd-Result or X-Rspamd-Score to produce a meaningful report - _, hasSpamdResult := headers["X-Spamd-Result"] - _, hasRspamdScore := headers["X-Rspamd-Score"] - if !hasSpamdResult && !hasRspamdScore { - return nil - } - - result := &api.RspamdResult{ - Symbols: make(map[string]api.SpamTestDetail), - } - - // Parse X-Spamd-Result header (primary source for score, threshold, and symbols) - // Format: "default: False [-3.91 / 15.00];\n\tSYMBOL(score)[params]; ..." - if spamdResult, ok := headers["X-Spamd-Result"]; ok { - report := strings.ReplaceAll(spamdResult, "; ", ";\n") - result.Report = &report - a.parseSpamdResult(spamdResult, result) - } - - // Parse X-Rspamd-Score as override/fallback for score - if scoreHeader, ok := headers["X-Rspamd-Score"]; ok { - if score, err := strconv.ParseFloat(strings.TrimSpace(scoreHeader), 64); err == nil { - result.Score = float32(score) - } - } - - // Parse X-Rspamd-Server - if serverHeader, ok := headers["X-Rspamd-Server"]; ok { - server := strings.TrimSpace(serverHeader) - result.Server = &server - } - - // Populate symbol descriptions from the lookup map - if a.symbols != nil { - for name, sym := range result.Symbols { - if desc, ok := a.symbols[name]; ok { - sym.Description = &desc - result.Symbols[name] = sym - } - } - } - - // Derive IsSpam from score vs reject threshold. - if result.Threshold > 0 { - result.IsSpam = result.Score >= result.Threshold - } else { - result.IsSpam = result.Score >= rspamdDefaultAddHeaderThreshold - } - - return result -} - -// parseSpamdResult parses the X-Spamd-Result header -// Format: "default: False [-3.91 / 15.00];\n\tSYMBOL(score)[params]; ..." -func (a *RspamdAnalyzer) parseSpamdResult(header string, result *api.RspamdResult) { - // Extract score and threshold from the first line - // e.g. "default: False [-3.91 / 15.00]" - scoreRe := regexp.MustCompile(`\[\s*(-?\d+\.?\d*)\s*/\s*(-?\d+\.?\d*)\s*\]`) - if matches := scoreRe.FindStringSubmatch(header); len(matches) > 2 { - if score, err := strconv.ParseFloat(matches[1], 64); err == nil { - result.Score = float32(score) - } - if threshold, err := strconv.ParseFloat(matches[2], 64); err == nil { - result.Threshold = float32(threshold) - - // No threshold? use default AddHeaderThreshold - if result.Threshold <= 0 { - result.Threshold = rspamdDefaultAddHeaderThreshold - } - } - } - - // Parse is_spam from header (before we may get action from X-Rspamd-Action) - firstLine := strings.SplitN(header, ";", 2)[0] - if strings.Contains(firstLine, ": True") || strings.Contains(firstLine, ": true") { - result.IsSpam = true - } - - // Parse symbols: SYMBOL(score)[params] - // Each symbol entry is separated by ";", so within each part we use a - // greedy match to capture params that may contain nested brackets. - symbolRe := regexp.MustCompile(`(\w+)\((-?\d+\.?\d*)\)(?:\[(.*)\])?`) - for _, part := range strings.Split(header, ";") { - part = strings.TrimSpace(part) - matches := symbolRe.FindStringSubmatch(part) - if len(matches) > 2 { - name := matches[1] - score, _ := strconv.ParseFloat(matches[2], 64) - sym := api.SpamTestDetail{ - Name: name, - Score: float32(score), - } - if len(matches) > 3 && matches[3] != "" { - params := matches[3] - sym.Params = ¶ms - } - result.Symbols[name] = sym - } - } -} - -// CalculateRspamdScore calculates the rspamd contribution to deliverability (0-100 scale) -func (a *RspamdAnalyzer) CalculateRspamdScore(result *api.RspamdResult) (int, string) { - if result == nil { - return 100, "" // rspamd not installed - } - - threshold := result.Threshold - percentage := 100 - int(math.Round(float64(result.Score*100/(2*threshold)))) - - if percentage > 100 { - return 100, "A+" - } else if percentage < 0 { - return 0, "F" - } - - // Linear scale between 0 and threshold - return percentage, ScoreToGrade(percentage) -} diff --git a/pkg/analyzer/rspamd_symbols.go b/pkg/analyzer/rspamd_symbols.go deleted file mode 100644 index e50a452..0000000 --- a/pkg/analyzer/rspamd_symbols.go +++ /dev/null @@ -1,105 +0,0 @@ -// This file is part of the happyDeliver (R) project. -// Copyright (c) 2026 happyDomain -// Authors: Pierre-Olivier Mercier, et al. -// -// This program is offered under a commercial and under the AGPL license. -// For commercial licensing, contact us at . -// -// For AGPL licensing: -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package analyzer - -import ( - _ "embed" - "encoding/json" - "io" - "log" - "net/http" - "strings" - "time" -) - -//go:embed rspamd-symbols.json -var embeddedRspamdSymbols []byte - -// rspamdSymbolGroup represents a group of rspamd symbols from the API/embedded JSON. -type rspamdSymbolGroup struct { - Group string `json:"group"` - Rules []rspamdSymbolEntry `json:"rules"` -} - -// rspamdSymbolEntry represents a single rspamd symbol entry. -type rspamdSymbolEntry struct { - Symbol string `json:"symbol"` - Description string `json:"description"` - Weight float64 `json:"weight"` -} - -// parseRspamdSymbolsJSON parses the rspamd symbols JSON into a name->description map. -func parseRspamdSymbolsJSON(data []byte) map[string]string { - var groups []rspamdSymbolGroup - if err := json.Unmarshal(data, &groups); err != nil { - log.Printf("Failed to parse rspamd symbols JSON: %v", err) - return nil - } - - symbols := make(map[string]string, len(groups)*10) - for _, g := range groups { - for _, r := range g.Rules { - if r.Description != "" { - symbols[r.Symbol] = r.Description - } - } - } - return symbols -} - -// LoadRspamdSymbols loads rspamd symbol descriptions. -// If apiURL is non-empty, it fetches from the rspamd API first, falling back to the embedded list on error. -func LoadRspamdSymbols(apiURL string) map[string]string { - if apiURL != "" { - if symbols := fetchRspamdSymbols(apiURL); symbols != nil { - return symbols - } - log.Printf("Failed to fetch rspamd symbols from %s, using embedded list", apiURL) - } - return parseRspamdSymbolsJSON(embeddedRspamdSymbols) -} - -// fetchRspamdSymbols fetches symbol descriptions from the rspamd API. -func fetchRspamdSymbols(apiURL string) map[string]string { - url := strings.TrimRight(apiURL, "/") + "/symbols" - - client := &http.Client{Timeout: 10 * time.Second} - resp, err := client.Get(url) - if err != nil { - log.Printf("Error fetching rspamd symbols: %v", err) - return nil - } - defer resp.Body.Close() - - if resp.StatusCode != http.StatusOK { - log.Printf("rspamd API returned status %d", resp.StatusCode) - return nil - } - - body, err := io.ReadAll(resp.Body) - if err != nil { - log.Printf("Error reading rspamd symbols response: %v", err) - return nil - } - - return parseRspamdSymbolsJSON(body) -} diff --git a/pkg/analyzer/rspamd_test.go b/pkg/analyzer/rspamd_test.go deleted file mode 100644 index 0eeca85..0000000 --- a/pkg/analyzer/rspamd_test.go +++ /dev/null @@ -1,414 +0,0 @@ -// This file is part of the happyDeliver (R) project. -// Copyright (c) 2026 happyDomain -// Authors: Pierre-Olivier Mercier, et al. -// -// This program is offered under a commercial and under the AGPL license. -// For commercial licensing, contact us at . -// -// For AGPL licensing: -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package analyzer - -import ( - "bytes" - "net/mail" - "testing" - - "git.happydns.org/happyDeliver/internal/api" -) - -func TestAnalyzeRspamdNoHeaders(t *testing.T) { - analyzer := NewRspamdAnalyzer(nil) - email := &EmailMessage{Header: make(mail.Header)} - - result := analyzer.AnalyzeRspamd(email) - - if result != nil { - t.Errorf("Expected nil for email without rspamd headers, got %+v", result) - } -} - -func TestParseSpamdResult(t *testing.T) { - tests := []struct { - name string - header string - expectedScore float32 - expectedThreshold float32 - expectedIsSpam bool - expectedSymbols map[string]float32 - expectedSymParams map[string]string - }{ - { - name: "Clean email negative score", - header: "default: False [-3.91 / 15.00];\n\tDATE_IN_PAST(0.10); ALL_TRUSTED(-1.00)[trusted]", - expectedScore: -3.91, - expectedThreshold: 15.00, - expectedIsSpam: false, - expectedSymbols: map[string]float32{ - "DATE_IN_PAST": 0.10, - "ALL_TRUSTED": -1.00, - }, - expectedSymParams: map[string]string{ - "ALL_TRUSTED": "trusted", - }, - }, - { - name: "Spam email True flag", - header: "default: True [16.50 / 15.00];\n\tBAYES_99(5.00)[1.00]; SPOOFED_SENDER(3.50)", - expectedScore: 16.50, - expectedThreshold: 15.00, - expectedIsSpam: true, - expectedSymbols: map[string]float32{ - "BAYES_99": 5.00, - "SPOOFED_SENDER": 3.50, - }, - expectedSymParams: map[string]string{ - "BAYES_99": "1.00", - }, - }, - { - name: "Zero threshold uses default", - header: "default: False [1.00 / 0.00]", - expectedScore: 1.00, - expectedThreshold: rspamdDefaultAddHeaderThreshold, - expectedIsSpam: false, - expectedSymbols: map[string]float32{}, - }, - { - name: "Symbol without params", - header: "default: False [2.00 / 15.00];\n\tMISSING_DATE(1.00)", - expectedScore: 2.00, - expectedThreshold: 15.00, - expectedIsSpam: false, - expectedSymbols: map[string]float32{ - "MISSING_DATE": 1.00, - }, - }, - { - name: "Case-insensitive true flag", - header: "default: true [8.00 / 6.00]", - expectedScore: 8.00, - expectedThreshold: 6.00, - expectedIsSpam: true, - expectedSymbols: map[string]float32{}, - }, - { - name: "Zero threshold with symbols containing nested brackets in params", - header: "default: False [0.90 / 0.00];\n" + - "\tARC_REJECT(1.00)[cannot verify 1 of 1 signatures: {[1] = sig:mail-tester.local:signature has incorrect length: 12}];\n" + - "\tMIME_GOOD(-0.10)[multipart/alternative,text/plain];\n" + - "\tMIME_TRACE(0.00)[0:+,1:+,2:~]", - expectedScore: 0.90, - expectedThreshold: rspamdDefaultAddHeaderThreshold, - expectedIsSpam: false, - expectedSymbols: map[string]float32{ - "ARC_REJECT": 1.00, - "MIME_GOOD": -0.10, - "MIME_TRACE": 0.00, - }, - expectedSymParams: map[string]string{ - "ARC_REJECT": "cannot verify 1 of 1 signatures: {[1] = sig:mail-tester.local:signature has incorrect length: 12}", - "MIME_GOOD": "multipart/alternative,text/plain", - "MIME_TRACE": "0:+,1:+,2:~", - }, - }, - } - - analyzer := NewRspamdAnalyzer(nil) - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := &api.RspamdResult{ - Symbols: make(map[string]api.SpamTestDetail), - } - analyzer.parseSpamdResult(tt.header, result) - - if result.Score != tt.expectedScore { - t.Errorf("Score = %v, want %v", result.Score, tt.expectedScore) - } - if result.Threshold != tt.expectedThreshold { - t.Errorf("Threshold = %v, want %v", result.Threshold, tt.expectedThreshold) - } - if result.IsSpam != tt.expectedIsSpam { - t.Errorf("IsSpam = %v, want %v", result.IsSpam, tt.expectedIsSpam) - } - for symName, expectedScore := range tt.expectedSymbols { - sym, ok := result.Symbols[symName] - if !ok { - t.Errorf("Symbol %s not found", symName) - continue - } - if sym.Score != expectedScore { - t.Errorf("Symbol %s score = %v, want %v", symName, sym.Score, expectedScore) - } - } - for symName, expectedParam := range tt.expectedSymParams { - sym, ok := result.Symbols[symName] - if !ok { - t.Errorf("Symbol %s not found for params check", symName) - continue - } - if sym.Params == nil { - t.Errorf("Symbol %s params = nil, want %q", symName, expectedParam) - } else if *sym.Params != expectedParam { - t.Errorf("Symbol %s params = %q, want %q", symName, *sym.Params, expectedParam) - } - } - }) - } -} - -func TestAnalyzeRspamd(t *testing.T) { - tests := []struct { - name string - headers map[string]string - expectedScore float32 - expectedThreshold float32 - expectedIsSpam bool - expectedServer *string - expectedSymCount int - }{ - { - name: "Full headers clean email", - headers: map[string]string{ - "X-Spamd-Result": "default: False [-3.91 / 15.00];\n\tALL_TRUSTED(-1.00)[local]", - "X-Rspamd-Score": "-3.91", - "X-Rspamd-Server": "mail.example.com", - }, - expectedScore: -3.91, - expectedThreshold: 15.00, - expectedIsSpam: false, - expectedServer: func() *string { s := "mail.example.com"; return &s }(), - expectedSymCount: 1, - }, - { - name: "X-Rspamd-Score overrides spamd result score", - headers: map[string]string{ - "X-Spamd-Result": "default: False [2.00 / 15.00]", - "X-Rspamd-Score": "3.50", - }, - expectedScore: 3.50, - expectedThreshold: 15.00, - expectedIsSpam: false, - }, - { - name: "Spam email above threshold", - headers: map[string]string{ - "X-Spamd-Result": "default: True [16.00 / 15.00];\n\tBAYES_99(5.00)", - "X-Rspamd-Score": "16.00", - }, - expectedScore: 16.00, - expectedThreshold: 15.00, - expectedIsSpam: true, - expectedSymCount: 1, - }, - { - name: "No X-Spamd-Result, only X-Rspamd-Score below default threshold", - headers: map[string]string{ - "X-Rspamd-Score": "2.00", - }, - expectedScore: 2.00, - expectedIsSpam: false, - }, - { - name: "No X-Spamd-Result, X-Rspamd-Score above default add-header threshold", - headers: map[string]string{ - "X-Rspamd-Score": "7.00", - }, - expectedScore: 7.00, - expectedIsSpam: true, - }, - { - name: "Server header is trimmed", - headers: map[string]string{ - "X-Rspamd-Score": "1.00", - "X-Rspamd-Server": " rspamd-01 ", - }, - expectedScore: 1.00, - expectedServer: func() *string { s := "rspamd-01"; return &s }(), - }, - } - - analyzer := NewRspamdAnalyzer(nil) - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - email := &EmailMessage{Header: make(mail.Header)} - for k, v := range tt.headers { - email.Header[k] = []string{v} - } - - result := analyzer.AnalyzeRspamd(email) - - if result == nil { - t.Fatal("Expected non-nil result") - } - if result.Score != tt.expectedScore { - t.Errorf("Score = %v, want %v", result.Score, tt.expectedScore) - } - if tt.expectedThreshold > 0 && result.Threshold != tt.expectedThreshold { - t.Errorf("Threshold = %v, want %v", result.Threshold, tt.expectedThreshold) - } - if result.IsSpam != tt.expectedIsSpam { - t.Errorf("IsSpam = %v, want %v", result.IsSpam, tt.expectedIsSpam) - } - if tt.expectedServer != nil { - if result.Server == nil { - t.Errorf("Server = nil, want %q", *tt.expectedServer) - } else if *result.Server != *tt.expectedServer { - t.Errorf("Server = %q, want %q", *result.Server, *tt.expectedServer) - } - } - if tt.expectedSymCount > 0 && len(result.Symbols) != tt.expectedSymCount { - t.Errorf("Symbol count = %d, want %d", len(result.Symbols), tt.expectedSymCount) - } - }) - } -} - -func TestCalculateRspamdScore(t *testing.T) { - tests := []struct { - name string - result *api.RspamdResult - expectedScore int - expectedGrade string - }{ - { - name: "Nil result (rspamd not installed)", - result: nil, - expectedScore: 100, - expectedGrade: "", - }, - { - name: "Score well below threshold", - result: &api.RspamdResult{ - Score: -3.91, - Threshold: 15.00, - }, - expectedScore: 100, - expectedGrade: "A+", - }, - { - name: "Score at zero", - result: &api.RspamdResult{ - Score: 0, - Threshold: 15.00, - }, - // 100 - round(0*100/30) = 100 → hits ScoreToGrade(100) = "A" - expectedScore: 100, - expectedGrade: "A", - }, - { - name: "Score at threshold (half of 2*threshold)", - result: &api.RspamdResult{ - Score: 15.00, - Threshold: 15.00, - }, - // 100 - round(15*100/(2*15)) = 100 - 50 = 50 - expectedScore: 50, - }, - { - name: "Score above 2*threshold", - result: &api.RspamdResult{ - Score: 31.00, - Threshold: 15.00, - }, - expectedScore: 0, - expectedGrade: "F", - }, - { - name: "Score exactly at 2*threshold", - result: &api.RspamdResult{ - Score: 30.00, - Threshold: 15.00, - }, - // 100 - round(30*100/30) = 100 - 100 = 0 - expectedScore: 0, - expectedGrade: "F", - }, - } - - analyzer := NewRspamdAnalyzer(nil) - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - score, grade := analyzer.CalculateRspamdScore(tt.result) - - if score != tt.expectedScore { - t.Errorf("Score = %d, want %d", score, tt.expectedScore) - } - if tt.expectedGrade != "" && grade != tt.expectedGrade { - t.Errorf("Grade = %q, want %q", grade, tt.expectedGrade) - } - }) - } -} - -const sampleEmailWithRspamdHeaders = `X-Spamd-Result: default: False [-3.91 / 15.00]; - BAYES_HAM(-3.00)[99%]; - RCVD_IN_DNSWL_MED(-0.01)[1.2.3.4:from]; - R_DKIM_ALLOW(-0.20)[example.com:s=dkim]; - FROM_HAS_DN(0.00)[]; - MIME_GOOD(-0.10)[text/plain]; -X-Rspamd-Score: -3.91 -X-Rspamd-Server: rspamd-01.example.com -Date: Mon, 09 Mar 2026 10:00:00 +0000 -From: sender@example.com -To: test@happydomain.org -Subject: Test email -Message-ID: -MIME-Version: 1.0 -Content-Type: text/plain - -Hello world` - -func TestAnalyzeRspamdRealEmail(t *testing.T) { - email, err := ParseEmail(bytes.NewBufferString(sampleEmailWithRspamdHeaders)) - if err != nil { - t.Fatalf("Failed to parse email: %v", err) - } - - analyzer := NewRspamdAnalyzer(nil) - result := analyzer.AnalyzeRspamd(email) - - if result == nil { - t.Fatal("Expected non-nil result") - } - if result.IsSpam { - t.Error("Expected IsSpam=false") - } - if result.Score != -3.91 { - t.Errorf("Score = %v, want -3.91", result.Score) - } - if result.Threshold != 15.00 { - t.Errorf("Threshold = %v, want 15.00", result.Threshold) - } - if result.Server == nil || *result.Server != "rspamd-01.example.com" { - t.Errorf("Server = %v, want \"rspamd-01.example.com\"", result.Server) - } - - expectedSymbols := []string{"BAYES_HAM", "RCVD_IN_DNSWL_MED", "R_DKIM_ALLOW", "FROM_HAS_DN", "MIME_GOOD"} - for _, sym := range expectedSymbols { - if _, ok := result.Symbols[sym]; !ok { - t.Errorf("Symbol %s not found", sym) - } - } - - score, _ := analyzer.CalculateRspamdScore(result) - if score != 100 { - t.Errorf("CalculateRspamdScore = %d, want 100", score) - } -} - diff --git a/pkg/analyzer/scoring.go b/pkg/analyzer/scoring.go index 5568c8e..ae91d4f 100644 --- a/pkg/analyzer/scoring.go +++ b/pkg/analyzer/scoring.go @@ -45,57 +45,7 @@ func ScoreToGrade(score int) string { } } -// ScoreToGradeKind converts a percentage score (0-100) to a letter grade, be kind in gradation -func ScoreToGradeKind(score int) string { - switch { - case score > 100: - return "A+" - case score >= 90: - return "A" - case score >= 80: - return "B" - case score >= 60: - return "C" - case score >= 45: - return "D" - case score >= 30: - return "E" - default: - return "F" - } -} - // ScoreToReportGrade converts a percentage score to an api.ReportGrade func ScoreToReportGrade(score int) api.ReportGrade { return api.ReportGrade(ScoreToGrade(score)) } - -// gradeRank returns a numeric rank for a grade (lower = worse) -func gradeRank(grade string) int { - switch grade { - case "A++": - return 7 - case "A+": - return 6 - case "A": - return 5 - case "B": - return 4 - case "C": - return 3 - case "D": - return 2 - case "E": - return 1 - default: - return 0 - } -} - -// MinGrade returns the minimal (worse) grade between the two given grades -func MinGrade(a, b string) string { - if gradeRank(a) <= gradeRank(b) { - return a - } - return b -} diff --git a/pkg/analyzer/spamassassin.go b/pkg/analyzer/spamassassin.go index d6ae961..cb80fe6 100644 --- a/pkg/analyzer/spamassassin.go +++ b/pkg/analyzer/spamassassin.go @@ -45,20 +45,12 @@ func (a *SpamAssassinAnalyzer) AnalyzeSpamAssassin(email *EmailMessage) *api.Spa return nil } - // Require at least X-Spam-Status, X-Spam-Score, or X-Spam-Flag to produce a meaningful report - _, hasStatus := headers["X-Spam-Status"] - _, hasScore := headers["X-Spam-Score"] - _, hasFlag := headers["X-Spam-Flag"] - if !hasStatus && !hasScore && !hasFlag { - return nil - } - result := &api.SpamAssassinResult{ TestDetails: make(map[string]api.SpamTestDetail), } // Parse X-Spam-Status header - if statusHeader, ok := headers["X-Spam-Status"]; ok && statusHeader != "" { + if statusHeader, ok := headers["X-Spam-Status"]; ok { a.parseSpamStatus(statusHeader, result) } diff --git a/web/package-lock.json b/web/package-lock.json index 6951a71..0911c63 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -13,65 +13,31 @@ "bootstrap-icons": "^1.13.1" }, "devDependencies": { - "@eslint/compat": "^2.0.0", - "@eslint/js": "^10.0.0", - "@hey-api/openapi-ts": "0.86.10", + "@eslint/compat": "^1.4.0", + "@eslint/js": "^9.36.0", + "@hey-api/openapi-ts": "0.86.4", "@sveltejs/adapter-static": "^3.0.9", "@sveltejs/kit": "^2.43.2", - "@sveltejs/vite-plugin-svelte": "^7.0.0", - "@types/node": "^24.0.0", - "eslint": "^10.0.0", + "@sveltejs/vite-plugin-svelte": "^6.2.0", + "@types/node": "^22", + "eslint": "^9.38.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-svelte": "^3.12.4", - "globals": "^17.0.0", + "globals": "^16.4.0", "prettier": "^3.6.2", "prettier-plugin-svelte": "^3.4.0", "svelte": "^5.39.5", "svelte-check": "^4.3.2", "typescript": "^5.9.2", "typescript-eslint": "^8.44.1", - "vite": "^8.0.0", + "vite": "^7.1.10", "vitest": "^3.2.4" } }, - "node_modules/@emnapi/core": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", - "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.0", - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", - "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", - "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", - "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", + "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", "cpu": [ "ppc64" ], @@ -86,9 +52,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", - "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", + "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", "cpu": [ "arm" ], @@ -103,9 +69,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", - "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", + "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", "cpu": [ "arm64" ], @@ -120,9 +86,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", - "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", + "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", "cpu": [ "x64" ], @@ -137,9 +103,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", - "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", + "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", "cpu": [ "arm64" ], @@ -154,9 +120,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", - "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", + "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", "cpu": [ "x64" ], @@ -171,9 +137,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", - "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", + "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", "cpu": [ "arm64" ], @@ -188,9 +154,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", - "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", + "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", "cpu": [ "x64" ], @@ -205,9 +171,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", - "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", + "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", "cpu": [ "arm" ], @@ -222,9 +188,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", - "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", + "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", "cpu": [ "arm64" ], @@ -239,9 +205,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", - "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", + "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", "cpu": [ "ia32" ], @@ -256,9 +222,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", - "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", + "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", "cpu": [ "loong64" ], @@ -273,9 +239,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", - "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", + "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", "cpu": [ "mips64el" ], @@ -290,9 +256,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", - "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", + "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", "cpu": [ "ppc64" ], @@ -307,9 +273,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", - "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", + "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", "cpu": [ "riscv64" ], @@ -324,9 +290,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", - "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", + "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", "cpu": [ "s390x" ], @@ -341,9 +307,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", - "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", + "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", "cpu": [ "x64" ], @@ -358,9 +324,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", - "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", + "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", "cpu": [ "arm64" ], @@ -375,9 +341,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", - "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", + "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", "cpu": [ "x64" ], @@ -392,9 +358,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", - "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", + "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", "cpu": [ "arm64" ], @@ -409,9 +375,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", - "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", + "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", "cpu": [ "x64" ], @@ -426,9 +392,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", - "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", + "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", "cpu": [ "arm64" ], @@ -443,9 +409,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", - "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", + "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", "cpu": [ "x64" ], @@ -460,9 +426,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", - "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", + "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", "cpu": [ "arm64" ], @@ -477,9 +443,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", - "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", + "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", "cpu": [ "ia32" ], @@ -494,9 +460,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", - "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", + "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", "cpu": [ "x64" ], @@ -511,9 +477,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { @@ -553,19 +519,19 @@ } }, "node_modules/@eslint/compat": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-2.0.3.tgz", - "integrity": "sha512-SjIJhGigp8hmd1YGIBwh7Ovri7Kisl42GYFjrOyHhtfYGGoLW6teYi/5p8W50KSsawUPpuLOSmsq1bD0NGQLBw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.4.0.tgz", + "integrity": "sha512-DEzm5dKeDBPm3r08Ixli/0cmxr8LkRdwxMRUIJBlSCpAwSrvFEJpVBzV+66JhDxiaqKxnRzCXhtiMiczF7Hglg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.1" + "@eslint/core": "^0.16.0" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "peerDependencies": { - "eslint": "^8.40 || 9 || 10" + "eslint": "^8.40 || 9" }, "peerDependenciesMeta": { "eslint": { @@ -574,95 +540,124 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.23.3", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.3.tgz", - "integrity": "sha512-j+eEWmB6YYLwcNOdlwQ6L2OsptI/LO6lNBuLIqe5R7RetD658HLoF+Mn7LzYmAWWNNzdC6cqP+L6r8ujeYXWLw==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^3.0.3", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", - "minimatch": "^10.2.4" + "minimatch": "^3.1.2" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/config-helpers": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", - "integrity": "sha512-lzGN0onllOZCGroKJmRwY6QcEHxbjBw1gwB8SgRSqK8YbbtEXMvKynsXc3553ckIEBxsbMBU7oOZXKIPGZNeZw==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz", + "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.1" + "@eslint/core": "^0.16.0" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.1.tgz", - "integrity": "sha512-QUPblTtE51/7/Zhfv8BDwO0qkkzQL7P/aWWbqcf4xWLEYn1oKjdO0gglQBB4GAsu7u6wjijbCmzsUTy6mnk6oQ==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", "dev": true, "license": "Apache-2.0", "dependencies": { "@types/json-schema": "^7.0.15" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@eslint/js": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-10.0.1.tgz", - "integrity": "sha512-zeR9k5pd4gxjZ0abRoIaxdc7I3nDktoXZk2qOv9gCNWx3mVwEn32VRhyLaRsDiJjTs0xq/T8mfPtyuXu7GWBcA==", + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, "license": "MIT", "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", + "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "eslint": "^10.0.0" - }, - "peerDependenciesMeta": { - "eslint": { - "optional": true - } } }, "node_modules/@eslint/object-schema": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.3.tgz", - "integrity": "sha512-iM869Pugn9Nsxbh/YHRqYiqd23AmIbxJOcpUMOuWCVNdoQJ5ZtwL6h3t0bcZzJUlC3Dq9jCFCESBZnX0GTv7iQ==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/plugin-kit": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", - "integrity": "sha512-iH1B076HoAshH1mLpHMgwdGeTs0CYwL0SPMkGuSebZrwBp16v415e9NZXg2jtrqPVQjf6IANe2Vtlr5KswtcZQ==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.1", + "@eslint/core": "^0.16.0", "levn": "^0.4.1" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@hey-api/codegen-core": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.3.3.tgz", - "integrity": "sha512-vArVDtrvdzFewu1hnjUm4jX1NBITlSCeO81EdWq676MxQbyxsGcDPAgohaSA+Wvr4HjPSvsg2/1s2zYxUtXebg==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@hey-api/codegen-core/-/codegen-core-0.3.1.tgz", + "integrity": "sha512-iLG9uRJdmQf83sCZ8WsDR6RXQep0X+D1t1mxuzhrSS9zVL4NvnjTQD6PNnQNPymJyss/mdPf7f7kbmcCK7DVmw==", "dev": true, "license": "MIT", "engines": { @@ -695,13 +690,13 @@ } }, "node_modules/@hey-api/openapi-ts": { - "version": "0.86.10", - "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.86.10.tgz", - "integrity": "sha512-Ns0dTJp/RUrOMPiJsO4/1E2Sa3VZ1iw2KCdG6PDbd9vLwOXEYW2UmiWMDPOTInLCYB+f8FLMF9T25jtfQe7AZg==", + "version": "0.86.4", + "resolved": "https://registry.npmjs.org/@hey-api/openapi-ts/-/openapi-ts-0.86.4.tgz", + "integrity": "sha512-TxQw+2IAykRrHlJwNU68rGjkuL92FhL4TDfkGCzj4dRxo+P4oiBOKSkxSNKUvolDQSdnsq1G71ynEkXoI7BJUg==", "dev": true, "license": "MIT", "dependencies": { - "@hey-api/codegen-core": "^0.3.2", + "@hey-api/codegen-core": "^0.3.1", "@hey-api/json-schema-ref-parser": "1.2.1", "ansi-colors": "4.1.3", "c12": "3.3.1", @@ -712,7 +707,7 @@ "semver": "7.7.2" }, "bin": { - "openapi-ts": "bin/run.js" + "openapi-ts": "bin/index.cjs" }, "engines": { "node": ">=20.19.0" @@ -833,31 +828,42 @@ "dev": true, "license": "MIT" }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", - "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1", - "@tybys/wasm-util": "^0.10.1" + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" + "engines": { + "node": ">= 8" } }, - "node_modules/@oxc-project/types": { - "version": "0.120.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.120.0.tgz", - "integrity": "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==", + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" } }, "node_modules/@polka/url": { @@ -878,272 +884,10 @@ "url": "https://opencollective.com/popperjs" } }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.10.tgz", - "integrity": "sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.10.tgz", - "integrity": "sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.10.tgz", - "integrity": "sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.10.tgz", - "integrity": "sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.10.tgz", - "integrity": "sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.10.tgz", - "integrity": "sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.10.tgz", - "integrity": "sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.10.tgz", - "integrity": "sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.10.tgz", - "integrity": "sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.10.tgz", - "integrity": "sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.10.tgz", - "integrity": "sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.10.tgz", - "integrity": "sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.10.tgz", - "integrity": "sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==", - "cpu": [ - "wasm32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "@napi-rs/wasm-runtime": "^1.1.1" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.10.tgz", - "integrity": "sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.10.tgz", - "integrity": "sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } - }, - "node_modules/@rolldown/pluginutils": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.10.tgz", - "integrity": "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==", - "dev": true, - "license": "MIT" - }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", - "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz", + "integrity": "sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ==", "cpu": [ "arm" ], @@ -1155,9 +899,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", - "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz", + "integrity": "sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA==", "cpu": [ "arm64" ], @@ -1169,9 +913,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", - "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz", + "integrity": "sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA==", "cpu": [ "arm64" ], @@ -1183,9 +927,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", - "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz", + "integrity": "sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA==", "cpu": [ "x64" ], @@ -1197,9 +941,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", - "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz", + "integrity": "sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA==", "cpu": [ "arm64" ], @@ -1211,9 +955,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", - "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz", + "integrity": "sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ==", "cpu": [ "x64" ], @@ -1225,9 +969,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", - "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz", + "integrity": "sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ==", "cpu": [ "arm" ], @@ -1239,9 +983,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", - "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz", + "integrity": "sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ==", "cpu": [ "arm" ], @@ -1253,9 +997,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", - "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz", + "integrity": "sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg==", "cpu": [ "arm64" ], @@ -1267,9 +1011,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", - "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz", + "integrity": "sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q==", "cpu": [ "arm64" ], @@ -1281,23 +1025,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", - "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", - "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz", + "integrity": "sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA==", "cpu": [ "loong64" ], @@ -1309,23 +1039,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", - "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", - "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz", + "integrity": "sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw==", "cpu": [ "ppc64" ], @@ -1337,9 +1053,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", - "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz", + "integrity": "sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw==", "cpu": [ "riscv64" ], @@ -1351,9 +1067,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", - "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz", + "integrity": "sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg==", "cpu": [ "riscv64" ], @@ -1365,9 +1081,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", - "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz", + "integrity": "sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ==", "cpu": [ "s390x" ], @@ -1379,9 +1095,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", - "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz", + "integrity": "sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q==", "cpu": [ "x64" ], @@ -1393,9 +1109,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", - "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz", + "integrity": "sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg==", "cpu": [ "x64" ], @@ -1406,24 +1122,10 @@ "linux" ] }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", - "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] - }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", - "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz", + "integrity": "sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw==", "cpu": [ "arm64" ], @@ -1435,9 +1137,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", - "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz", + "integrity": "sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w==", "cpu": [ "arm64" ], @@ -1449,9 +1151,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", - "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz", + "integrity": "sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg==", "cpu": [ "ia32" ], @@ -1463,9 +1165,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", - "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz", + "integrity": "sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ==", "cpu": [ "x64" ], @@ -1477,9 +1179,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", - "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz", + "integrity": "sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg==", "cpu": [ "x64" ], @@ -1491,16 +1193,16 @@ ] }, "node_modules/@standard-schema/spec": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", - "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", "dev": true, "license": "MIT" }, "node_modules/@sveltejs/acorn-typescript": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.9.tgz", - "integrity": "sha512-lVJX6qEgs/4DOcRTpo56tmKzVPtoWAaVbL4hfO7t7NVwl9AAXzQR6cihesW1BmNMPl+bK6dreu2sOKBP2Q9CIA==", + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@sveltejs/acorn-typescript/-/acorn-typescript-1.0.6.tgz", + "integrity": "sha512-4awhxtMh4cx9blePWl10HRHj8Iivtqj+2QdDCSMDzxG+XKa9+VCNupQuCuvzEhYPzZSrX+0gC+0lHA/0fFKKQQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -1518,23 +1220,25 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.55.0", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.55.0.tgz", - "integrity": "sha512-MdFRjevVxmAknf2NbaUkDF16jSIzXMWd4Nfah0Qp8TtQVoSp3bV4jKt8mX7z7qTUTWvgSaxtR0EG5WJf53gcuA==", + "version": "2.48.0", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.48.0.tgz", + "integrity": "sha512-GAAbkWrbRJvysL7+HOWs5v/+TmnRcEQPeED2sUcDFTHpPvRYADEtScL6x8hWuKp0DKauJVaVJLTjQVy9e7cMiw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@standard-schema/spec": "^1.0.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/cookie": "^0.6.0", "acorn": "^8.14.1", "cookie": "^0.6.0", - "devalue": "^5.6.4", + "devalue": "^5.3.2", "esm-env": "^1.2.2", "kleur": "^4.1.5", "magic-string": "^0.30.5", "mrmime": "^2.0.0", - "set-cookie-parser": "^3.0.0", + "sade": "^1.8.1", + "set-cookie-parser": "^2.6.0", "sirv": "^3.0.0" }, "bin": { @@ -1545,49 +1249,54 @@ }, "peerDependencies": { "@opentelemetry/api": "^1.0.0", - "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0 || ^7.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1 || ^5.0.0 || ^6.0.0-next.0", "svelte": "^4.0.0 || ^5.0.0-next.0", - "typescript": "^5.3.3", - "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0 || ^8.0.0" + "vite": "^5.0.3 || ^6.0.0 || ^7.0.0-beta.0" }, "peerDependenciesMeta": { "@opentelemetry/api": { "optional": true - }, - "typescript": { - "optional": true } } }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-7.0.0.tgz", - "integrity": "sha512-ILXmxC7HAsnkK2eslgPetrqqW1BKSL7LktsFgqzNj83MaivMGZzluWq32m25j2mDOjmSKX7GGWahePhuEs7P/g==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.1.tgz", + "integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { + "@sveltejs/vite-plugin-svelte-inspector": "^5.0.0", + "debug": "^4.4.1", "deepmerge": "^4.3.1", - "magic-string": "^0.30.21", - "obug": "^2.1.0", - "vitefu": "^1.1.2" + "magic-string": "^0.30.17", + "vitefu": "^1.1.1" }, "engines": { "node": "^20.19 || ^22.12 || >=24" }, "peerDependencies": { - "svelte": "^5.46.4", - "vite": "^8.0.0-beta.7 || ^8.0.0" + "svelte": "^5.0.0", + "vite": "^6.3.0 || ^7.0.0" } }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", - "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "node_modules/@sveltejs/vite-plugin-svelte-inspector": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.1.tgz", + "integrity": "sha512-ubWshlMk4bc8mkwWbg6vNvCeT7lGQojE3ijDh3QTR6Zr/R+GXxsGbyH4PExEPpiFmqPhYiVSVmHBjUcVc1JIrA==", "dev": true, "license": "MIT", - "optional": true, "dependencies": { - "tslib": "^2.4.0" + "debug": "^4.4.1" + }, + "engines": { + "node": "^20.19 || ^22.12 || >=24" + }, + "peerDependencies": { + "@sveltejs/vite-plugin-svelte": "^6.0.0-next.0", + "svelte": "^5.0.0", + "vite": "^6.3.0 || ^7.0.0" } }, "node_modules/@types/chai": { @@ -1615,13 +1324,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/esrecurse": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1637,37 +1339,32 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.12.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", - "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "version": "22.18.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.18.13.tgz", + "integrity": "sha512-Bo45YKIjnmFtv6I1TuC8AaHBbqXtIo+Om5fE4QiU1Tj8QR/qt+8O3BAtOimG5IFmwaWiPmB3Mv3jtYzBA4Us2A==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "undici-types": "~7.16.0" + "undici-types": "~6.21.0" } }, - "node_modules/@types/trusted-types": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", - "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", - "dev": true, - "license": "MIT" - }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz", - "integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.2.tgz", + "integrity": "sha512-ZGBMToy857/NIPaaCucIUQgqueOiq7HeAKkhlvqVV4lm089zUFW6ikRySx2v+cAhKeUCPuWVHeimyk6Dw1iY3w==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/type-utils": "8.57.1", - "@typescript-eslint/utils": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", - "ignore": "^7.0.5", + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/type-utils": "8.46.2", + "@typescript-eslint/utils": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1677,8 +1374,8 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.57.1", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "@typescript-eslint/parser": "^8.46.2", + "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, @@ -1693,17 +1390,18 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.1.tgz", - "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.2.tgz", + "integrity": "sha512-BnOroVl1SgrPLywqxyqdJ4l3S2MsKVLDVxZvjI1Eoe8ev2r3kGDo+PcMihNmDE+6/KjkTubSJnmqGZZjQSBq/g==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", - "debug": "^4.4.3" + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "debug": "^4.3.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1713,20 +1411,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.1.tgz", - "integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.2.tgz", + "integrity": "sha512-PULOLZ9iqwI7hXcmL4fVfIsBi6AN9YxRc0frbvmg8f+4hQAjQ5GYNKK0DIArNo+rOKmR/iBYwkpBmnIwin4wBg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.1", - "@typescript-eslint/types": "^8.57.1", - "debug": "^4.4.3" + "@typescript-eslint/tsconfig-utils": "^8.46.2", + "@typescript-eslint/types": "^8.46.2", + "debug": "^4.3.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1740,14 +1438,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz", - "integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.2.tgz", + "integrity": "sha512-LF4b/NmGvdWEHD2H4MsHD8ny6JpiVNDzrSZr3CsckEgCbAGZbYM4Cqxvi9L+WqDMT+51Ozy7lt2M+d0JLEuBqA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1" + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1758,9 +1456,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz", - "integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.2.tgz", + "integrity": "sha512-a7QH6fw4S57+F5y2FIxxSDyi5M4UfGF+Jl1bCGd7+L4KsaUY80GsiF/t0UoRFDHAguKlBaACWJRmdrc6Xfkkag==", "dev": true, "license": "MIT", "engines": { @@ -1775,17 +1473,17 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz", - "integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.2.tgz", + "integrity": "sha512-HbPM4LbaAAt/DjxXaG9yiS9brOOz6fabal4uvUmaUYe6l3K1phQDMQKBRUrr06BQkxkvIZVVHttqiybM9nJsLA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/utils": "8.57.1", - "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/utils": "8.46.2", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1795,14 +1493,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.1.tgz", - "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.2.tgz", + "integrity": "sha512-lNCWCbq7rpg7qDsQrd3D6NyWYu+gkTENkG5IKYhUIcxSb59SQC/hEQ+MrG4sTgBVghTonNWq42bA/d4yYumldQ==", "dev": true, "license": "MIT", "engines": { @@ -1814,21 +1512,22 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz", - "integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.2.tgz", + "integrity": "sha512-f7rW7LJ2b7Uh2EiQ+7sza6RDZnajbNbemn54Ob6fRwQbgcIn+GWfyuHDHRYgRoZu1P4AayVScrRW+YfbTvPQoQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.57.1", - "@typescript-eslint/tsconfig-utils": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" + "@typescript-eslint/project-service": "8.46.2", + "@typescript-eslint/tsconfig-utils": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/visitor-keys": "8.46.2", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1841,30 +1540,43 @@ "typescript": ">=4.8.4 <6.0.0" } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@typescript-eslint/utils": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.1.tgz", - "integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1" + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.2.tgz", + "integrity": "sha512-sExxzucx0Tud5tE0XqR0lT0psBQvEpnpiul9XbGUB1QwpWJJAps1O/Z7hJxLGiZLBKMCutjTzDgmd1muEhBnVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.2", + "@typescript-eslint/types": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1874,19 +1586,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz", - "integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.2.tgz", + "integrity": "sha512-tUFMXI4gxzzMXt4xpGJEsBsTox0XbNQ1y94EwlD/CuZwFcQP79xfQqMhau9HsRc/J0cAPA/HZt1dZPtGn9V/7w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.1", - "eslint-visitor-keys": "^5.0.0" + "@typescript-eslint/types": "8.46.2", + "eslint-visitor-keys": "^4.2.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1913,6 +1625,33 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, "node_modules/@vitest/pretty-format": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", @@ -1985,11 +1724,12 @@ } }, "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2008,9 +1748,9 @@ } }, "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, "license": "MIT", "dependencies": { @@ -2034,6 +1774,22 @@ "node": ">=6" } }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -2042,9 +1798,9 @@ "license": "Python-2.0" }, "node_modules/aria-query": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.1.tgz", - "integrity": "sha512-Z/ZeOgVl7bcSYZ/u/rh0fOpvEpq//LZmdbkXyc7syVzjPAhfOa9ebsdTSjEBDU4vs5nC98Kfduj1uFo0qyET3g==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2072,14 +1828,11 @@ } }, "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } + "license": "MIT" }, "node_modules/bootstrap": { "version": "5.3.8", @@ -2117,16 +1870,27 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", - "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^4.0.2" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" }, "engines": { - "node": "18 || 20 || >=22" + "node": ">=8" } }, "node_modules/bundle-name": { @@ -2184,6 +1948,16 @@ "node": ">=8" } }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/chai": { "version": "5.3.3", "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", @@ -2201,10 +1975,27 @@ "node": ">=18" } }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/check-error": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", - "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "dev": true, "license": "MIT", "engines": { @@ -2247,6 +2038,26 @@ "node": ">=6" } }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -2267,10 +2078,17 @@ "node": ">=20" } }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/confbox": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", - "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", "dev": true, "license": "MIT" }, @@ -2368,9 +2186,9 @@ } }, "node_modules/default-browser": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.5.0.tgz", - "integrity": "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", "dev": true, "license": "MIT", "dependencies": { @@ -2385,9 +2203,9 @@ } }, "node_modules/default-browser-id": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz", - "integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", "dev": true, "license": "MIT", "engines": { @@ -2424,27 +2242,17 @@ "dev": true, "license": "MIT" }, - "node_modules/detect-libc": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", - "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=8" - } - }, "node_modules/devalue": { - "version": "5.6.4", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz", - "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.4.2.tgz", + "integrity": "sha512-MwPZTKEPK2k8Qgfmqrd48ZKVvzSQjgW0lXLxiIBA8dQjtf/6mw6pggHNLcyDKyf+fI6eXxlQwPsfaCMTU5U+Bw==", "dev": true, "license": "MIT" }, "node_modules/dotenv": { - "version": "17.3.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", - "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "version": "17.2.3", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz", + "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==", "dev": true, "license": "BSD-2-Clause", "engines": { @@ -2462,9 +2270,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", - "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", + "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2475,32 +2283,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.4", - "@esbuild/android-arm": "0.27.4", - "@esbuild/android-arm64": "0.27.4", - "@esbuild/android-x64": "0.27.4", - "@esbuild/darwin-arm64": "0.27.4", - "@esbuild/darwin-x64": "0.27.4", - "@esbuild/freebsd-arm64": "0.27.4", - "@esbuild/freebsd-x64": "0.27.4", - "@esbuild/linux-arm": "0.27.4", - "@esbuild/linux-arm64": "0.27.4", - "@esbuild/linux-ia32": "0.27.4", - "@esbuild/linux-loong64": "0.27.4", - "@esbuild/linux-mips64el": "0.27.4", - "@esbuild/linux-ppc64": "0.27.4", - "@esbuild/linux-riscv64": "0.27.4", - "@esbuild/linux-s390x": "0.27.4", - "@esbuild/linux-x64": "0.27.4", - "@esbuild/netbsd-arm64": "0.27.4", - "@esbuild/netbsd-x64": "0.27.4", - "@esbuild/openbsd-arm64": "0.27.4", - "@esbuild/openbsd-x64": "0.27.4", - "@esbuild/openharmony-arm64": "0.27.4", - "@esbuild/sunos-x64": "0.27.4", - "@esbuild/win32-arm64": "0.27.4", - "@esbuild/win32-ia32": "0.27.4", - "@esbuild/win32-x64": "0.27.4" + "@esbuild/aix-ppc64": "0.25.11", + "@esbuild/android-arm": "0.25.11", + "@esbuild/android-arm64": "0.25.11", + "@esbuild/android-x64": "0.25.11", + "@esbuild/darwin-arm64": "0.25.11", + "@esbuild/darwin-x64": "0.25.11", + "@esbuild/freebsd-arm64": "0.25.11", + "@esbuild/freebsd-x64": "0.25.11", + "@esbuild/linux-arm": "0.25.11", + "@esbuild/linux-arm64": "0.25.11", + "@esbuild/linux-ia32": "0.25.11", + "@esbuild/linux-loong64": "0.25.11", + "@esbuild/linux-mips64el": "0.25.11", + "@esbuild/linux-ppc64": "0.25.11", + "@esbuild/linux-riscv64": "0.25.11", + "@esbuild/linux-s390x": "0.25.11", + "@esbuild/linux-x64": "0.25.11", + "@esbuild/netbsd-arm64": "0.25.11", + "@esbuild/netbsd-x64": "0.25.11", + "@esbuild/openbsd-arm64": "0.25.11", + "@esbuild/openbsd-x64": "0.25.11", + "@esbuild/openharmony-arm64": "0.25.11", + "@esbuild/sunos-x64": "0.25.11", + "@esbuild/win32-arm64": "0.25.11", + "@esbuild/win32-ia32": "0.25.11", + "@esbuild/win32-x64": "0.25.11" } }, "node_modules/escape-string-regexp": { @@ -2517,30 +2325,34 @@ } }, "node_modules/eslint": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.1.0.tgz", - "integrity": "sha512-S9jlY/ELKEUwwQnqWDO+f+m6sercqOPSqXM5Go94l7DOmxHVDgmSFGWEzeE/gwgTAr0W103BWt0QLe/7mabIvA==", + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", + "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.3", - "@eslint/config-helpers": "^0.5.3", - "@eslint/core": "^1.1.1", - "@eslint/plugin-kit": "^0.6.1", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.1", + "@eslint/core": "^0.16.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.38.0", + "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "ajv": "^6.14.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.2", - "eslint-visitor-keys": "^5.0.1", - "espree": "^11.2.0", - "esquery": "^1.7.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", @@ -2550,7 +2362,8 @@ "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.4", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -2558,7 +2371,7 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://eslint.org/donate" @@ -2589,9 +2402,9 @@ } }, "node_modules/eslint-plugin-svelte": { - "version": "3.16.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.16.0.tgz", - "integrity": "sha512-DJXxqpYZUxcE0SfYo8EJzV2ZC+zAD7fJp1n1HwcEMRR1cOEUYvjT9GuzJeNghMjgb7uxuK3IJAzI+x6zzUxO5A==", + "version": "3.12.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.12.5.tgz", + "integrity": "sha512-4KRG84eAHQfYd9OjZ1K7sCHy0nox+9KwT+s5WCCku3jTim5RV4tVENob274nCwIaApXsYPKAUAZFBxKZ3Wyfjw==", "dev": true, "license": "MIT", "dependencies": { @@ -2613,7 +2426,7 @@ "url": "https://github.com/sponsors/ota-meshi" }, "peerDependencies": { - "eslint": "^8.57.1 || ^9.0.0 || ^10.0.0", + "eslint": "^8.57.1 || ^9.0.0", "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0" }, "peerDependenciesMeta": { @@ -2622,46 +2435,31 @@ } } }, - "node_modules/eslint-plugin-svelte/node_modules/globals": { - "version": "16.5.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", - "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint-scope": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -2675,27 +2473,27 @@ "license": "MIT" }, "node_modules/espree": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.16.0", + "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" + "eslint-visitor-keys": "^4.2.1" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -2706,14 +2504,13 @@ } }, "node_modules/esrap": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.4.tgz", - "integrity": "sha512-suICpxAmZ9A8bzJjEl/+rLJiDKC0X4gYWUxT6URAWBLvlXmtbZd5ySMu/N2ZGEtMCAmflUDPSehrP9BQcsGcSg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.1.tgz", + "integrity": "sha512-ebTT9B6lOtZGMgJ3o5r12wBacHctG7oEWazIda8UlPfA3HD/Wrv8FdXoVo73vzdpwCxNyXjPauyN2bbJzMkB9A==", "dev": true, "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15", - "@typescript-eslint/types": "^8.2.0" + "@jridgewell/sourcemap-codec": "^1.4.15" } }, "node_modules/esrecurse": { @@ -2760,9 +2557,9 @@ } }, "node_modules/expect-type": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", - "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2770,9 +2567,9 @@ } }, "node_modules/exsolve": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.8.tgz", - "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", "dev": true, "license": "MIT" }, @@ -2783,6 +2580,36 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", @@ -2797,6 +2624,16 @@ "dev": true, "license": "MIT" }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2828,6 +2665,19 @@ "node": ">=16.0.0" } }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/find-up": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", @@ -2860,9 +2710,9 @@ } }, "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, "license": "ISC" }, @@ -2913,9 +2763,9 @@ } }, "node_modules/globals": { - "version": "17.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-17.4.0.tgz", - "integrity": "sha512-hjrNztw/VajQwOLsMNT1cbJiH2muO3OROCHnbehc8eY5JyD2gqz4AcMHPqgaOR59DjgUjYAYLeH699g/eWi2jw==", + "version": "16.4.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.4.0.tgz", + "integrity": "sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==", "dev": true, "license": "MIT", "engines": { @@ -2925,6 +2775,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -2947,6 +2804,16 @@ "uglify-js": "^3.1.4" } }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2957,6 +2824,23 @@ "node": ">= 4" } }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -3025,6 +2909,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-reference": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-reference/-/is-reference-3.0.3.tgz", @@ -3036,9 +2930,9 @@ } }, "node_modules/is-wsl": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", - "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", "dev": true, "license": "MIT", "dependencies": { @@ -3076,9 +2970,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, "license": "MIT", "dependencies": { @@ -3150,267 +3044,6 @@ "node": ">= 0.8.0" } }, - "node_modules/lightningcss": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", - "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", - "dev": true, - "license": "MPL-2.0", - "dependencies": { - "detect-libc": "^2.0.3" - }, - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - }, - "optionalDependencies": { - "lightningcss-android-arm64": "1.32.0", - "lightningcss-darwin-arm64": "1.32.0", - "lightningcss-darwin-x64": "1.32.0", - "lightningcss-freebsd-x64": "1.32.0", - "lightningcss-linux-arm-gnueabihf": "1.32.0", - "lightningcss-linux-arm64-gnu": "1.32.0", - "lightningcss-linux-arm64-musl": "1.32.0", - "lightningcss-linux-x64-gnu": "1.32.0", - "lightningcss-linux-x64-musl": "1.32.0", - "lightningcss-win32-arm64-msvc": "1.32.0", - "lightningcss-win32-x64-msvc": "1.32.0" - } - }, - "node_modules/lightningcss-android-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", - "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-arm64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", - "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-darwin-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", - "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-freebsd-x64": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", - "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", - "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", - "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", - "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", - "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-linux-x64-musl": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", - "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", - "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.32.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", - "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MPL-2.0", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, "node_modules/lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", @@ -3445,9 +3078,16 @@ } }, "node_modules/lodash": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", - "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true, "license": "MIT" }, @@ -3468,20 +3108,54 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, - "node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", "dev": true, - "license": "BlueOak-1.0.0", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", "dependencies": { - "brace-expansion": "^5.0.2" + "braces": "^3.0.3", + "picomatch": "^2.3.1" }, "engines": { - "node": "18 || 20 || >=22" + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" }, "funding": { - "url": "https://github.com/sponsors/isaacs" + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" } }, "node_modules/minimist": { @@ -3562,41 +3236,25 @@ "license": "MIT" }, "node_modules/nypm": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.5.tgz", - "integrity": "sha512-K6AJy1GMVyfyMXRVB88700BJqNUkByijGJM8kEHpLdcAt+vSQAVfkWWHYzuRXHSY6xA2sNc5RjTj0p9rE2izVQ==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.2.tgz", + "integrity": "sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==", "dev": true, "license": "MIT", "dependencies": { - "citty": "^0.2.0", + "citty": "^0.1.6", + "consola": "^3.4.2", "pathe": "^2.0.3", - "tinyexec": "^1.0.2" + "pkg-types": "^2.3.0", + "tinyexec": "^1.0.1" }, "bin": { "nypm": "dist/cli.mjs" }, "engines": { - "node": ">=18" + "node": "^14.16.0 || >=16.10.0" } }, - "node_modules/nypm/node_modules/citty": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.1.tgz", - "integrity": "sha512-kEV95lFBhQgtogAPlQfJJ0WGVSokvLr/UEoFPiKKOXF7pl98HfUVUD0ejsuTCld/9xH9vogSywZ5KqHzXrZpqg==", - "dev": true, - "license": "MIT" - }, - "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", - "dev": true, - "funding": [ - "https://github.com/sponsors/sxzz", - "https://opencollective.com/debug" - ], - "license": "MIT" - }, "node_modules/ohash": { "version": "2.0.11", "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", @@ -3673,6 +3331,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3711,9 +3382,9 @@ } }, "node_modules/perfect-debounce": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.1.0.tgz", - "integrity": "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-2.0.0.tgz", + "integrity": "sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow==", "dev": true, "license": "MIT" }, @@ -3730,6 +3401,7 @@ "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", + "peer": true, "engines": { "node": ">=12" }, @@ -3750,9 +3422,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "dev": true, "funding": [ { @@ -3769,6 +3441,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3809,9 +3482,9 @@ } }, "node_modules/postcss-load-config/node_modules/yaml": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.3.tgz", - "integrity": "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA==", + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", "dev": true, "license": "ISC", "engines": { @@ -3873,9 +3546,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", - "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", + "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", "dev": true, "license": "MIT", "dependencies": { @@ -3897,11 +3570,12 @@ } }, "node_modules/prettier": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", - "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", "dev": true, "license": "MIT", + "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -3913,9 +3587,9 @@ } }, "node_modules/prettier-plugin-svelte": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.5.1.tgz", - "integrity": "sha512-65+fr5+cgIKWKiqM1Doum4uX6bY8iFCdztvvp2RcF+AJoieaw9kJOFMNcJo/bkmKYsxFaM9OsVZK/gWauG/5mg==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.4.0.tgz", + "integrity": "sha512-pn1ra/0mPObzqoIQn/vUTR3ZZI6UuZ0sHqMK5x2jMLGrs53h0sXhkVuDcrlssHwIMk7FYrMjHBPoUSyyEEDlBQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -3933,6 +3607,27 @@ "node": ">=6" } }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, "node_modules/rc9": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", @@ -3958,44 +3653,31 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/rolldown": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.10.tgz", - "integrity": "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==", + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, "license": "MIT", - "dependencies": { - "@oxc-project/types": "=0.120.0", - "@rolldown/pluginutils": "1.0.0-rc.10" - }, - "bin": { - "rolldown": "bin/cli.mjs" - }, "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.0.0-rc.10", - "@rolldown/binding-darwin-arm64": "1.0.0-rc.10", - "@rolldown/binding-darwin-x64": "1.0.0-rc.10", - "@rolldown/binding-freebsd-x64": "1.0.0-rc.10", - "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.10", - "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.10", - "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.10", - "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.10", - "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.10", - "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.10", - "@rolldown/binding-linux-x64-musl": "1.0.0-rc.10", - "@rolldown/binding-openharmony-arm64": "1.0.0-rc.10", - "@rolldown/binding-wasm32-wasi": "1.0.0-rc.10", - "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.10", - "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10" + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" } }, "node_modules/rollup": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", - "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", + "version": "4.52.5", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.52.5.tgz", + "integrity": "sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw==", "dev": true, "license": "MIT", "dependencies": { @@ -4009,31 +3691,28 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.0", - "@rollup/rollup-android-arm64": "4.60.0", - "@rollup/rollup-darwin-arm64": "4.60.0", - "@rollup/rollup-darwin-x64": "4.60.0", - "@rollup/rollup-freebsd-arm64": "4.60.0", - "@rollup/rollup-freebsd-x64": "4.60.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", - "@rollup/rollup-linux-arm-musleabihf": "4.60.0", - "@rollup/rollup-linux-arm64-gnu": "4.60.0", - "@rollup/rollup-linux-arm64-musl": "4.60.0", - "@rollup/rollup-linux-loong64-gnu": "4.60.0", - "@rollup/rollup-linux-loong64-musl": "4.60.0", - "@rollup/rollup-linux-ppc64-gnu": "4.60.0", - "@rollup/rollup-linux-ppc64-musl": "4.60.0", - "@rollup/rollup-linux-riscv64-gnu": "4.60.0", - "@rollup/rollup-linux-riscv64-musl": "4.60.0", - "@rollup/rollup-linux-s390x-gnu": "4.60.0", - "@rollup/rollup-linux-x64-gnu": "4.60.0", - "@rollup/rollup-linux-x64-musl": "4.60.0", - "@rollup/rollup-openbsd-x64": "4.60.0", - "@rollup/rollup-openharmony-arm64": "4.60.0", - "@rollup/rollup-win32-arm64-msvc": "4.60.0", - "@rollup/rollup-win32-ia32-msvc": "4.60.0", - "@rollup/rollup-win32-x64-gnu": "4.60.0", - "@rollup/rollup-win32-x64-msvc": "4.60.0", + "@rollup/rollup-android-arm-eabi": "4.52.5", + "@rollup/rollup-android-arm64": "4.52.5", + "@rollup/rollup-darwin-arm64": "4.52.5", + "@rollup/rollup-darwin-x64": "4.52.5", + "@rollup/rollup-freebsd-arm64": "4.52.5", + "@rollup/rollup-freebsd-x64": "4.52.5", + "@rollup/rollup-linux-arm-gnueabihf": "4.52.5", + "@rollup/rollup-linux-arm-musleabihf": "4.52.5", + "@rollup/rollup-linux-arm64-gnu": "4.52.5", + "@rollup/rollup-linux-arm64-musl": "4.52.5", + "@rollup/rollup-linux-loong64-gnu": "4.52.5", + "@rollup/rollup-linux-ppc64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-gnu": "4.52.5", + "@rollup/rollup-linux-riscv64-musl": "4.52.5", + "@rollup/rollup-linux-s390x-gnu": "4.52.5", + "@rollup/rollup-linux-x64-gnu": "4.52.5", + "@rollup/rollup-linux-x64-musl": "4.52.5", + "@rollup/rollup-openharmony-arm64": "4.52.5", + "@rollup/rollup-win32-arm64-msvc": "4.52.5", + "@rollup/rollup-win32-ia32-msvc": "4.52.5", + "@rollup/rollup-win32-x64-gnu": "4.52.5", + "@rollup/rollup-win32-x64-msvc": "4.52.5", "fsevents": "~2.3.2" } }, @@ -4050,6 +3729,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, "node_modules/sade": { "version": "1.8.1", "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", @@ -4077,9 +3780,9 @@ } }, "node_modules/set-cookie-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-3.1.0.tgz", - "integrity": "sha512-kjnC1DXBHcxaOaOXBHBeRtltsDG2nUiUni+jP92M9gYdW12rsmx92UsfpH7o5tDRs7I1ZZPSQJQGv3UaRfCiuw==", + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz", + "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==", "dev": true, "license": "MIT" }, @@ -4162,6 +3865,19 @@ "dev": true, "license": "MIT" }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/strip-literal": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", @@ -4175,25 +3891,37 @@ "url": "https://github.com/sponsors/antfu" } }, - "node_modules/svelte": { - "version": "5.54.1", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.54.1.tgz", - "integrity": "sha512-ow8tncN097Ty8U1H+C3bM1xNlsCbnO2UZeN0lWBnv8f3jKho7QTTQ2LWbMXrPQDodLjH91n4kpNnLolyRhVE6A==", + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/svelte": { + "version": "5.42.2", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.42.2.tgz", + "integrity": "sha512-iSry5jsBHispVczyt9UrBX/1qu3HQ/UyKPAIjqlvlu3o/eUvc+kpyMyRS2O4HLLx4MvLurLGIUOyyP11pyD59g==", + "dev": true, + "license": "MIT", + "peer": true, "dependencies": { "@jridgewell/remapping": "^2.3.4", "@jridgewell/sourcemap-codec": "^1.5.0", "@sveltejs/acorn-typescript": "^1.0.5", "@types/estree": "^1.0.5", - "@types/trusted-types": "^2.0.7", "acorn": "^8.12.1", - "aria-query": "5.3.1", + "aria-query": "^5.3.1", "axobject-query": "^4.1.0", "clsx": "^2.1.1", - "devalue": "^5.6.4", "esm-env": "^1.2.1", - "esrap": "^2.2.2", + "esrap": "^2.1.0", "is-reference": "^3.0.3", "locate-character": "^3.0.0", "magic-string": "^0.30.11", @@ -4204,9 +3932,9 @@ } }, "node_modules/svelte-check": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.4.5.tgz", - "integrity": "sha512-1bSwIRCvvmSHrlK52fOlZmVtUZgil43jNL/2H18pRpa+eQjzGt6e3zayxhp1S7GajPFKNM/2PMCG+DZFHlG9fw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.3.tgz", + "integrity": "sha512-RYP0bEwenDXzfv0P1sKAwjZSlaRyqBn0Fz1TVni58lqyEiqgwztTpmodJrGzP6ZT2aHl4MbTvWP6gbmQ3FOnBg==", "dev": true, "license": "MIT", "dependencies": { @@ -4228,9 +3956,9 @@ } }, "node_modules/svelte-eslint-parser": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.6.0.tgz", - "integrity": "sha512-qoB1ehychT6OxEtQAqc/guSqLS20SlA53Uijl7x375s8nlUT0lb9ol/gzraEEatQwsyPTJo87s2CmKL9Xab+Uw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-1.4.0.tgz", + "integrity": "sha512-fjPzOfipR5S7gQ/JvI9r2H8y9gMGXO3JtmrylHLLyahEMquXI0lrebcjT+9/hNgDej0H7abTyox5HpHmW1PSWA==", "dev": true, "license": "MIT", "dependencies": { @@ -4239,12 +3967,11 @@ "espree": "^10.0.0", "postcss": "^8.4.49", "postcss-scss": "^4.0.9", - "postcss-selector-parser": "^7.0.0", - "semver": "^7.7.2" + "postcss-selector-parser": "^7.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0", - "pnpm": "10.30.3" + "pnpm": "10.18.3" }, "funding": { "url": "https://github.com/sponsors/ota-meshi" @@ -4258,54 +3985,6 @@ } } }, - "node_modules/svelte-eslint-parser/node_modules/eslint-scope": { - "version": "8.4.0", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", - "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/svelte-eslint-parser/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/svelte-eslint-parser/node_modules/espree": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", - "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.15.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.2.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/tinybench": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", @@ -4314,14 +3993,11 @@ "license": "MIT" }, "node_modules/tinyexec": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", - "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", + "integrity": "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==", "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - } + "license": "MIT" }, "node_modules/tinyglobby": { "version": "0.2.15", @@ -4370,6 +4046,19 @@ "node": ">=14.0.0" } }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -4381,9 +4070,9 @@ } }, "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", "dev": true, "license": "MIT", "engines": { @@ -4393,14 +4082,6 @@ "typescript": ">=4.8.4" } }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, - "license": "0BSD", - "optional": true - }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -4420,6 +4101,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4429,16 +4111,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.57.1.tgz", - "integrity": "sha512-fLvZWf+cAGw3tqMCYzGIU6yR8K+Y9NT2z23RwOjlNFF2HwSB3KhdEFI5lSBv8tNmFkkBShSjsCjzx1vahZfISA==", + "version": "8.46.2", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.2.tgz", + "integrity": "sha512-vbw8bOmiuYNdzzV3lsiWv6sRwjyuKJMQqWulBOU7M0RrxedXledX8G8kBbQeiOYDnTfiXz0Y4081E1QMNB6iQg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.57.1", - "@typescript-eslint/parser": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/utils": "8.57.1" + "@typescript-eslint/eslint-plugin": "8.46.2", + "@typescript-eslint/parser": "8.46.2", + "@typescript-eslint/typescript-estree": "8.46.2", + "@typescript-eslint/utils": "8.46.2" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4448,7 +4130,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, @@ -4467,9 +4149,9 @@ } }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, @@ -4491,114 +4173,14 @@ "license": "MIT" }, "node_modules/vite": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.1.tgz", - "integrity": "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==", + "version": "7.1.12", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.1.12.tgz", + "integrity": "sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.8", - "rolldown": "1.0.0-rc.10", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.1.0", - "esbuild": "^0.27.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, - "node_modules/vite-node": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", - "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.4.1", - "es-module-lexer": "^1.7.0", - "pathe": "^2.0.3", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || ^20.0.0 || >=22.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite-node/node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.27.0", + "esbuild": "^0.25.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", @@ -4666,10 +4248,33 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/vitefu": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.2.tgz", - "integrity": "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.1.tgz", + "integrity": "sha512-B/Fegf3i8zh0yFbpzZ21amWzHmuNlLlmJT6n7bu5e+pCHUKQIfXSYokrqOBGEMMe9UG2sostKQF9mml/vYaWJQ==", "dev": true, "license": "MIT", "workspaces": [ @@ -4678,7 +4283,7 @@ "tests/projects/workspace/packages/*" ], "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" }, "peerDependenciesMeta": { "vite": { @@ -4759,33 +4364,6 @@ } } }, - "node_modules/vitest/node_modules/@vitest/mocker": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/spy": "3.2.4", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" - }, - "funding": { - "url": "https://opencollective.com/vitest" - }, - "peerDependencies": { - "msw": "^2.4.9", - "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" - }, - "peerDependenciesMeta": { - "msw": { - "optional": true - }, - "vite": { - "optional": true - } - } - }, "node_modules/vitest/node_modules/tinyexec": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", @@ -4793,81 +4371,6 @@ "dev": true, "license": "MIT" }, - "node_modules/vitest/node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", - "dev": true, - "license": "MIT", - "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" - }, - "bin": { - "vite": "bin/vite.js" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - }, - "funding": { - "url": "https://github.com/vitejs/vite?sponsor=1" - }, - "optionalDependencies": { - "fsevents": "~2.3.3" - }, - "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "jiti": ">=1.21.0", - "less": "^4.0.0", - "lightningcss": "^1.21.0", - "sass": "^1.70.0", - "sass-embedded": "^1.70.0", - "stylus": ">=0.54.8", - "sugarss": "^5.0.0", - "terser": "^5.16.0", - "tsx": "^4.8.1", - "yaml": "^2.4.2" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - }, - "jiti": { - "optional": true - }, - "less": { - "optional": true - }, - "lightningcss": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "stylus": { - "optional": true - }, - "sugarss": { - "optional": true - }, - "terser": { - "optional": true - }, - "tsx": { - "optional": true - }, - "yaml": { - "optional": true - } - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -4934,22 +4437,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/yaml": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", - "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", - "extraneous": true, - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/web/package.json b/web/package.json index fce3e61..8deb4f4 100644 --- a/web/package.json +++ b/web/package.json @@ -16,24 +16,24 @@ "generate:api": "openapi-ts" }, "devDependencies": { - "@eslint/compat": "^2.0.0", - "@eslint/js": "^10.0.0", - "@hey-api/openapi-ts": "0.86.10", + "@eslint/compat": "^1.4.0", + "@eslint/js": "^9.36.0", + "@hey-api/openapi-ts": "0.86.4", "@sveltejs/adapter-static": "^3.0.9", "@sveltejs/kit": "^2.43.2", - "@sveltejs/vite-plugin-svelte": "^7.0.0", - "@types/node": "^24.0.0", - "eslint": "^10.0.0", + "@sveltejs/vite-plugin-svelte": "^6.2.0", + "@types/node": "^22", + "eslint": "^9.38.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-svelte": "^3.12.4", - "globals": "^17.0.0", + "globals": "^16.4.0", "prettier": "^3.6.2", "prettier-plugin-svelte": "^3.4.0", "svelte": "^5.39.5", "svelte-check": "^4.3.2", "typescript": "^5.9.2", "typescript-eslint": "^8.44.1", - "vite": "^8.0.0", + "vite": "^7.1.10", "vitest": "^3.2.4" }, "dependencies": { diff --git a/web/routes.go b/web/routes.go index 876954c..c60cb11 100644 --- a/web/routes.go +++ b/web/routes.go @@ -27,6 +27,7 @@ import ( "fmt" "io" "io/fs" + "io/ioutil" "log" "net/http" "net/url" @@ -62,14 +63,6 @@ func DeclareRoutes(cfg *config.Config, router *gin.Engine) { appConfig["survey_url"] = cfg.SurveyURL.String() } - if len(cfg.Analysis.RBLs) > 0 { - appConfig["rbls"] = cfg.Analysis.RBLs - } - - if cfg.CustomLogoURL != "" { - appConfig["custom_logo_url"] = cfg.CustomLogoURL - } - if appcfg, err := json.MarshalIndent(appConfig, "", " "); err != nil { log.Println("Unable to generate JSON config to inject in web application") } else { @@ -89,12 +82,6 @@ func DeclareRoutes(cfg *config.Config, router *gin.Engine) { router.GET("/_app/immutable/*_", func(c *gin.Context) { c.Writer.Header().Set("Cache-Control", "public, max-age=604800, immutable") }, serveOrReverse("", cfg)) router.GET("/", serveOrReverse("/", cfg)) - router.GET("/blacklist/", serveOrReverse("/", cfg)) - router.GET("/blacklist/:ip", serveOrReverse("/", cfg)) - router.GET("/domain/", serveOrReverse("/", cfg)) - router.GET("/domain/:domain", serveOrReverse("/", cfg)) - router.GET("/test/", serveOrReverse("/", cfg)) - router.GET("/test/:testid", serveOrReverse("/", cfg)) router.GET("/favicon.png", func(c *gin.Context) { c.Writer.Header().Set("Cache-Control", "public, max-age=604800, immutable") }, serveOrReverse("", cfg)) router.GET("/img/*path", serveOrReverse("", cfg)) @@ -143,7 +130,7 @@ func serveOrReverse(forced_url string, cfg *config.Config) gin.HandlerFunc { } } - v, _ := io.ReadAll(resp.Body) + v, _ := ioutil.ReadAll(resp.Body) v2 := strings.Replace(strings.Replace(string(v), "", `{{ .Head }}`, 1), "", "{{ .Body }}", 1) @@ -170,7 +157,7 @@ func serveOrReverse(forced_url string, cfg *config.Config) gin.HandlerFunc { if indexTpl == nil { // Create template from file f, _ := Assets.Open("index.html") - v, _ := io.ReadAll(f) + v, _ := ioutil.ReadAll(f) v2 := strings.Replace(strings.Replace(string(v), "", `{{ .Head }}`, 1), "", "{{ .Body }}", 1) diff --git a/web/src/app.css b/web/src/app.css index dca80a5..1472994 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -1,9 +1,6 @@ :root { --bs-primary: #1cb487; --bs-primary-rgb: 28, 180, 135; - --bs-link-color-rgb: 28, 180, 135; - --bs-link-hover-color-rgb: 17, 112, 84; - --bs-tertiary-bg: #e7e8e8; } body { @@ -11,10 +8,6 @@ body { -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } -.bg-tertiary { - background-color: var(--bs-tertiary-bg); -} - /* Animations */ @keyframes fadeIn { from { diff --git a/web/src/lib/components/AuthenticationCard.svelte b/web/src/lib/components/AuthenticationCard.svelte index 46a4d2d..0b36dd0 100644 --- a/web/src/lib/components/AuthenticationCard.svelte +++ b/web/src/lib/components/AuthenticationCard.svelte @@ -13,26 +13,13 @@ let { authentication, authenticationGrade, authenticationScore, dnsResults }: Props = $props(); - let allRequiredMissing = $derived( - !authentication.spf && - (!authentication.dkim || authentication.dkim.length === 0) && - !authentication.dmarc, - ); - function getAuthResultClass(result: string, noneIsFail: boolean): string { switch (result) { case "pass": - case "domain_pass": - case "orgdomain_pass": return "text-success"; - case "permerror": - case "error": case "fail": case "missing": case "invalid": - case "null": - case "null_smtp": - case "null_header": return "text-danger"; case "softfail": case "neutral": @@ -49,19 +36,12 @@ function getAuthResultIcon(result: string, noneIsFail: boolean): string { switch (result) { case "pass": - case "domain_pass": - case "orgdomain_pass": return "bi-check-circle-fill"; case "fail": return "bi-x-circle-fill"; case "softfail": case "neutral": case "invalid": - case "null": - case "permerror": - case "error": - case "null_smtp": - case "null_header": return "bi-exclamation-circle-fill"; case "missing": return "bi-dash-circle-fill"; @@ -103,465 +83,282 @@ - {#if allRequiredMissing} -
-
- - No authentication results found. -

- This usually means either: -

-
    -
  • - The receiving mail server is not configured to verify email authentication - (no Authentication-Results header was found in the message). -
  • -
  • - The Authentication-Results header exists but the receiver - hostname does not match the configured - --receiver-hostname value. -
  • -
-
-
- {/if}
- - {#if authentication.iprev} -
-
- -
- IP Reverse DNS - - {authentication.iprev.result} - - {#if authentication.iprev.ip} -
- IP Address: - {authentication.iprev.ip} -
- {/if} - {#if authentication.iprev.hostname} -
- Hostname: - {authentication.iprev.hostname} -
- {/if} - {#if authentication.iprev.details} -
{authentication.iprev.details}
- {/if} -
-
-
- {/if} - - -
-
- {#if authentication.spf} - -
- SPF - - {authentication.spf.result} - - {#if authentication.spf.domain} -
- Domain: - {authentication.spf.domain} -
- {/if} - {#if authentication.spf.details} -
{authentication.spf.details}
- {/if} -
- {:else} - -
- SPF - - {getAuthResultText("missing")} - -
- SPF record is required for proper email authentication -
-
- {/if} -
-
- - -
- {#if authentication.dkim && authentication.dkim.length > 0} - {#each authentication.dkim as dkim, i} -
0}> - + + {#if authentication.iprev} +
+
+
- DKIM{authentication.dkim.length > 1 ? ` #${i + 1}` : ""} - - {dkim.result} + IP Reverse DNS + + {authentication.iprev.result} - {#if dkim.domain} + {#if authentication.iprev.ip}
- Domain: - {dkim.domain} + IP Address: + {authentication.iprev.ip}
{/if} - {#if dkim.selector} + {#if authentication.iprev.hostname}
- Selector: - {dkim.selector} + Hostname: + {authentication.iprev.hostname}
{/if} - {#if dkim.details} -
{dkim.details}
+ {#if authentication.iprev.details} +
{authentication.iprev.details}
{/if}
- {/each} - {:else} -
- -
- DKIM - - {getAuthResultText("missing")} - -
- DKIM signature is required for proper email authentication -
-
{/if} -
- - {#if authentication.x_google_dkim} -
-
- -
- X-Google-DKIM - - - {authentication.x_google_dkim.result} - - {#if authentication.x_google_dkim.domain} -
- Domain: - {authentication.x_google_dkim.domain} -
- {/if} - {#if authentication.x_google_dkim.selector} -
- Selector: - {authentication.x_google_dkim.selector} -
- {/if} - {#if authentication.x_google_dkim.details} -
{authentication.x_google_dkim
-                                    .details}
- {/if} -
-
-
- {/if} - - - {#if authentication.x_aligned_from} -
-
- -
- X-Aligned-From - - - {authentication.x_aligned_from.result} - - {#if authentication.x_aligned_from.domain} -
- Domain: - {authentication.x_aligned_from.domain} -
- {/if} - {#if authentication.x_aligned_from.details} -
{authentication.x_aligned_from
-                                    .details}
- {/if} -
-
-
- {/if} - - -
-
- {#if authentication.dmarc} - -
- DMARC - - {authentication.dmarc.result} - - {#if authentication.dmarc.domain} -
- Domain: - {authentication.dmarc.domain} -
- {/if} - {#snippet DMARCPolicy(policy: string)} -
- Policy: - - {policy} - -
- {/snippet} - {#if authentication.dmarc.result != "none"} - {#if authentication.dmarc.details && authentication.dmarc.details.indexOf("policy.published-domain-policy=") > 0} - {@const policy = authentication.dmarc.details.replace( - /^.*policy.published-domain-policy=([^\s]+).*$/, - "$1", - )} - {@render DMARCPolicy(policy)} - {:else if authentication.dmarc.domain && dnsResults?.dmarc_record?.policy} - {@render DMARCPolicy(dnsResults.dmarc_record.policy)} + +
+
+ {#if authentication.spf} + +
+ SPF + + {authentication.spf.result} + + {#if authentication.spf.domain} +
+ Domain: + {authentication.spf.domain} +
+ {/if} + {#if authentication.spf.details} +
{authentication.spf.details}
{/if} - {/if} - {#if authentication.dmarc.details} -
{authentication.dmarc.details}
- {/if} -
- {:else} - -
- DMARC - - {getAuthResultText("missing")} - -
- DMARC policy is required for proper email authentication
-
- {/if} -
-
- - -
-
- {#if authentication.bimi && authentication.bimi.result != "none"} - -
- BIMI - - {authentication.bimi.result} - - {#if authentication.bimi.details} -
{authentication.bimi.details}
- {/if} -
- {:else if authentication.bimi && authentication.bimi.result == "none"} - -
- BIMI - NONE -
- Brand Indicators for Message Identification + {:else} + +
+ SPF + + {getAuthResultText('missing')} + +
SPF record is required for proper email authentication
- {#if authentication.bimi.details} -
{authentication.bimi.details}
- {/if} -
- {:else} - -
- BIMI - Optional -
- Brand Indicators for Message Identification (optional enhancement) -
-
- {/if} -
-
- - - {#if authentication.arc} -
-
- -
- ARC - - {authentication.arc.result} - - {#if authentication.arc.chain_length} -
- Chain length: {authentication.arc.chain_length} -
- {/if} - {#if authentication.arc.details} -
{authentication.arc.details}
- {/if} -
+ {/if}
- {/if} + + +
+ {#if authentication.dkim && authentication.dkim.length > 0} + {#each authentication.dkim as dkim, i} +
0}> + +
+ DKIM{authentication.dkim.length > 1 ? ` #${i + 1}` : ''} + + {dkim.result} + + {#if dkim.domain} +
+ Domain: + {dkim.domain} +
+ {/if} + {#if dkim.selector} +
+ Selector: + {dkim.selector} +
+ {/if} + {#if dkim.details} +
{dkim.details}
+ {/if} +
+
+ {/each} + {:else} +
+ +
+ DKIM + + {getAuthResultText('missing')} + +
DKIM signature is required for proper email authentication
+
+
+ {/if} +
+ + + {#if authentication.x_google_dkim} +
+
+ +
+ X-Google-DKIM + + + {authentication.x_google_dkim.result} + + {#if authentication.x_google_dkim.domain} +
+ Domain: + {authentication.x_google_dkim.domain} +
+ {/if} + {#if authentication.x_google_dkim.selector} +
+ Selector: + {authentication.x_google_dkim.selector} +
+ {/if} + {#if authentication.x_google_dkim.details} +
{authentication.x_google_dkim.details}
+ {/if} +
+
+
+ {/if} + + + {#if authentication.x_aligned_from} +
+
+ +
+ X-Aligned-From + + + {authentication.x_aligned_from.result} + + {#if authentication.x_aligned_from.domain} +
+ Domain: + {authentication.x_aligned_from.domain} +
+ {/if} + {#if authentication.x_aligned_from.details} +
{authentication.x_aligned_from.details}
+ {/if} +
+
+
+ {/if} + + +
+
+ {#if authentication.dmarc} + +
+ DMARC + + {authentication.dmarc.result} + + {#if authentication.dmarc.domain} +
+ Domain: + {authentication.dmarc.domain} +
+ {/if} + {#snippet DMARCPolicy(policy: string)} +
+ Policy: + + {policy} + +
+ {/snippet} + {#if authentication.dmarc.result != "none"} + {#if authentication.dmarc.details && authentication.dmarc.details.indexOf("policy.published-domain-policy=") > 0} + {@const policy = authentication.dmarc.details.replace(/^.*policy.published-domain-policy=([^\s]+).*$/, "$1")} + {@render DMARCPolicy(policy)} + {:else if authentication.dmarc.domain && dnsResults?.dmarc_record?.policy} + {@render DMARCPolicy(dnsResults.dmarc_record.policy)} + {/if} + {/if} + {#if authentication.dmarc.details} +
{authentication.dmarc.details}
+ {/if} +
+ {:else} + +
+ DMARC + + {getAuthResultText('missing')} + +
DMARC policy is required for proper email authentication
+
+ {/if} +
+
+ + +
+
+ {#if authentication.bimi && authentication.bimi.result != "none"} + +
+ BIMI + + {authentication.bimi.result} + + {#if authentication.bimi.details} +
{authentication.bimi.details}
+ {/if} +
+ {:else if authentication.bimi && authentication.bimi.result == "none"} + +
+ BIMI + + NONE + +
Brand Indicators for Message Identification
+ {#if authentication.bimi.details} +
{authentication.bimi.details}
+ {/if} +
+ {:else} + +
+ BIMI + + Optional + +
Brand Indicators for Message Identification (optional enhancement)
+
+ {/if} +
+
+ + + {#if authentication.arc} +
+
+ +
+ ARC + + {authentication.arc.result} + + {#if authentication.arc.chain_length} +
Chain length: {authentication.arc.chain_length}
+ {/if} + {#if authentication.arc.details} +
{authentication.arc.details}
+ {/if} +
+
+
+ {/if}
diff --git a/web/src/lib/components/BlacklistCard.svelte b/web/src/lib/components/BlacklistCard.svelte index bb80acb..bb0a160 100644 --- a/web/src/lib/components/BlacklistCard.svelte +++ b/web/src/lib/components/BlacklistCard.svelte @@ -1,21 +1,27 @@
-
-

+
+

Blacklist Checks @@ -33,7 +39,11 @@

-
+ {#if receivedChain} + + {/if} + +
{#each Object.entries(blacklists) as [ip, checks]}
@@ -44,19 +54,9 @@ {#each checks as check} - - - {check.error - ? "Error" - : check.listed - ? "Listed" - : "Clean"} + + + {check.error ? 'Error' : (check.listed ? 'Listed' : 'Clean')} {check.rbl} diff --git a/web/src/lib/components/ContentAnalysisCard.svelte b/web/src/lib/components/ContentAnalysisCard.svelte index 51c4e5b..87cfd5e 100644 --- a/web/src/lib/components/ContentAnalysisCard.svelte +++ b/web/src/lib/components/ContentAnalysisCard.svelte @@ -36,28 +36,16 @@
- + HTML Part
- + Plaintext Part
- {#if typeof contentAnalysis.has_unsubscribe_link === "boolean"} + {#if typeof contentAnalysis.has_unsubscribe_link === 'boolean'}
- + Unsubscribe Link
{/if} @@ -86,14 +74,7 @@
Content Issues
{#each contentAnalysis.html_issues as issue} -
+
{issue.type} @@ -137,17 +118,11 @@ {/if} - + {link.status} - {link.http_code || "-"} + {link.http_code || '-'} {/each} @@ -171,11 +146,11 @@ {#each contentAnalysis.images as image} - {image.src || "-"} + {image.src || '-'} {#if image.has_alt} - {image.alt_text || "Present"} + {image.alt_text || 'Present'} {:else} Missing diff --git a/web/src/lib/components/DmarcRecordDisplay.svelte b/web/src/lib/components/DmarcRecordDisplay.svelte index b7a3e7b..09a10c7 100644 --- a/web/src/lib/components/DmarcRecordDisplay.svelte +++ b/web/src/lib/components/DmarcRecordDisplay.svelte @@ -34,10 +34,9 @@

- DMARC enforces domain alignment requirements (regardless of the policy). It builds - on SPF and DKIM by telling receiving servers what to do with emails that fail - authentication checks. It also enables reporting so you can monitor your email - security. + DMARC builds on SPF and DKIM by telling receiving servers what to do with emails + that fail authentication checks. It also enables reporting so you can monitor your + email security.


diff --git a/web/src/lib/components/DnsRecordsCard.svelte b/web/src/lib/components/DnsRecordsCard.svelte index b7997b0..2b3c99c 100644 --- a/web/src/lib/components/DnsRecordsCard.svelte +++ b/web/src/lib/components/DnsRecordsCard.svelte @@ -1,15 +1,15 @@ {#if receivedChain && receivedChain.length > 0} -
-
-

- - Email Path -

-
-
+
+
Email Path (Received Chain)
+
{#each receivedChain as hop, i}
{receivedChain.length - i} - {hop.reverse || "-"} - {#if hop.ip}({hop.ip}){/if} → {hop.by || - "Unknown"} + {hop.reverse || '-'} {#if hop.ip}({hop.ip}){/if} → {hop.by || 'Unknown'}
- - {hop.timestamp - ? new Intl.DateTimeFormat("default", { - dateStyle: "long", - timeStyle: "short", - }).format(new Date(hop.timestamp)) - : "-"} - + {hop.timestamp ? new Intl.DateTimeFormat('default', { dateStyle: 'long', 'timeStyle': 'short' }).format(new Date(hop.timestamp)) : '-'}
- {#if hop.with || hop.id || hop.from} + {#if hop.with || hop.id}

{#if hop.with} - Protocol: - {hop.with} + Protocol: {hop.with} {/if} {#if hop.id} diff --git a/web/src/lib/components/GradeDisplay.svelte b/web/src/lib/components/GradeDisplay.svelte index f9d1f78..2cae341 100644 --- a/web/src/lib/components/GradeDisplay.svelte +++ b/web/src/lib/components/GradeDisplay.svelte @@ -44,7 +44,10 @@ } - + {#if grade} {grade} {:else} diff --git a/web/src/lib/components/HeaderAnalysisCard.svelte b/web/src/lib/components/HeaderAnalysisCard.svelte index b26b492..36e173b 100644 --- a/web/src/lib/components/HeaderAnalysisCard.svelte +++ b/web/src/lib/components/HeaderAnalysisCard.svelte @@ -1,5 +1,5 @@ {#if ptrRecords && ptrRecords.length > 0} @@ -68,31 +63,15 @@

Forward Resolution (A/AAAA): {#each ptrForwardRecords as ip} - {#if ip === senderIp || !fcrDnsIsValid || showDifferent} -
- {#if senderIp && ip === senderIp} - Match - {:else} - Different - {/if} - {ip} -
- {/if} - {/each} - {#if fcrDnsIsValid && differentCount > 0} -
- +
+ {#if senderIp && ip === senderIp} + Match + {:else} + Different + {/if} + {ip}
- {/if} + {/each}
{#if fcrDnsIsValid}
diff --git a/web/src/lib/components/RspamdCard.svelte b/web/src/lib/components/RspamdCard.svelte deleted file mode 100644 index 4c2795b..0000000 --- a/web/src/lib/components/RspamdCard.svelte +++ /dev/null @@ -1,153 +0,0 @@ - - -
-
-

- - - rspamd Analysis - - - {#if rspamd.deliverability_score !== undefined} - - {rspamd.deliverability_score}% - - {/if} - {#if rspamd.deliverability_grade !== undefined} - - {/if} - -

-
-
-
-
- Score: - - {rspamd.score.toFixed(2)} / {rspamd.threshold.toFixed(1)} - -
-
- Classified as: - - {rspamd.is_spam ? "SPAM" : "HAM"} - -
-
- Action: - - {effectiveAction.label} - -
-
- - {#if rspamd.symbols && Object.keys(rspamd.symbols).length > 0} -
-
- - - - - - - - - - {#each Object.entries(rspamd.symbols).sort(([, a], [, b]) => b.score - a.score) as [symbolName, symbol]} - 0 - ? "table-warning" - : symbol.score < 0 - ? "table-success" - : ""} - > - - - - - {/each} - -
SymbolScoreDescription
- {symbolName} - {#if symbol.params} - - {symbol.params} - - {/if} - - 0 - ? "text-danger fw-bold" - : symbol.score < 0 - ? "text-success fw-bold" - : "text-muted"} - > - {symbol.score > 0 ? "+" : ""}{symbol.score.toFixed(2)} - - {symbol.description ?? ""}
-
-
- {/if} - - {#if rspamd.report} -
- Raw Report -
{rspamd.report}
-
- {/if} -
-
- - diff --git a/web/src/lib/components/ScoreCard.svelte b/web/src/lib/components/ScoreCard.svelte index 7a80dc4..e6ce6be 100644 --- a/web/src/lib/components/ScoreCard.svelte +++ b/web/src/lib/components/ScoreCard.svelte @@ -1,7 +1,7 @@
@@ -19,13 +21,13 @@ SpamAssassin Analysis - {#if spamassassin.deliverability_score !== undefined} - - {spamassassin.deliverability_score}% + {#if spamScore !== undefined} + + {spamScore}% {/if} - {#if spamassassin.deliverability_grade !== undefined} - + {#if spamGrade !== undefined} + {/if}
@@ -59,26 +61,14 @@ {#each Object.entries(spamassassin.test_details) as [testName, detail]} - 0 - ? "table-warning" - : detail.score < 0 - ? "table-success" - : ""} - > + 0 ? 'table-warning' : detail.score < 0 ? 'table-success' : ''}> {testName} - 0 - ? "text-danger fw-bold" - : detail.score < 0 - ? "text-success fw-bold" - : "text-muted"} - > - {detail.score > 0 ? "+" : ""}{detail.score.toFixed(1)} + 0 ? 'text-danger fw-bold' : detail.score < 0 ? 'text-success fw-bold' : 'text-muted'}> + {detail.score > 0 ? '+' : ''}{detail.score.toFixed(1)} - {detail.description || ""} + {detail.description || ''} {/each} @@ -90,11 +80,7 @@ Tests Triggered:
{#each spamassassin.tests as test} - {test} + {test} {/each}
@@ -103,10 +89,7 @@ {#if spamassassin.report}
Raw Report -
{spamassassin.report}
+
{spamassassin.report}
{/if}
diff --git a/web/src/lib/components/SpfRecordsDisplay.svelte b/web/src/lib/components/SpfRecordsDisplay.svelte index 2ebb2c2..f9dd738 100644 --- a/web/src/lib/components/SpfRecordsDisplay.svelte +++ b/web/src/lib/components/SpfRecordsDisplay.svelte @@ -11,8 +11,8 @@ // Check if DMARC has strict policy (quarantine or reject) const dmarcStrict = $derived( dmarcRecord?.valid && - dmarcRecord?.policy && - (dmarcRecord.policy === "quarantine" || dmarcRecord.policy === "reject"), + dmarcRecord?.policy && + (dmarcRecord.policy === "quarantine" || dmarcRecord.policy === "reject") ); // Compute overall validity @@ -43,11 +43,7 @@ SPF
-

- SPF specifies which mail servers are authorized to send emails on behalf of your - domain. Receiving servers check the sender's IP address against your SPF record to - prevent email spoofing. -

+

SPF specifies which mail servers are authorized to send emails on behalf of your domain. Receiving servers check the sender's IP address against your SPF record to prevent email spoofing.

{#each spfRecords as spf, index} @@ -80,31 +76,18 @@ {:else if spf.all_qualifier === "?"} Neutral (?all) {/if} - {#if index === 0 || (index === 1 && spfRecords[0].record?.includes("redirect="))} -
- {#if spf.all_qualifier === "-"} - All unauthorized servers will be rejected. This is the - recommended strict policy. + {#if index === 0 || (index === 1 && spfRecords[0].record?.includes('redirect='))} +
+ {#if spf.all_qualifier === '-'} + All unauthorized servers will be rejected. This is the recommended strict policy. {:else if dmarcStrict} - While your DMARC {dmarcRecord?.policy} policy provides some protection, - consider using -all for better security with some - old mailbox providers. - {:else if spf.all_qualifier === "~"} - Unauthorized servers will softfail. Consider using -all for stricter policy, though this rarely affects legitimate - email deliverability. - {:else if spf.all_qualifier === "+"} - All servers are allowed to send email. This severely weakens - email authentication. Use -all for strict policy. - {:else if spf.all_qualifier === "?"} - No statement about unauthorized servers. Use -all for strict policy to prevent spoofing. + While your DMARC {dmarcRecord?.policy} policy provides some protection, consider using -all for better security with some old mailbox providers. + {:else if spf.all_qualifier === '~'} + Unauthorized servers will softfail. Consider using -all for stricter policy, though this rarely affects legitimate email deliverability. + {:else if spf.all_qualifier === '+'} + All servers are allowed to send email. This severely weakens email authentication. Use -all for strict policy. + {:else if spf.all_qualifier === '?'} + No statement about unauthorized servers. Use -all for strict policy to prevent spoofing. {/if}
{/if} @@ -112,16 +95,14 @@ {/if} {#if spf.record}
- Record:
+ Record:
{spf.record}
{/if} {#if spf.error}
- - {spf.valid ? "Warning:" : "Error:"} - {spf.error} + + {spf.valid ? 'Warning:' : 'Error:'} {spf.error}
{/if}
diff --git a/web/src/lib/components/SummaryCard.svelte b/web/src/lib/components/SummaryCard.svelte index 518e996..1267f8b 100644 --- a/web/src/lib/components/SummaryCard.svelte +++ b/web/src/lib/components/SummaryCard.svelte @@ -25,32 +25,16 @@ // Email sender information const mailFrom = report.header_analysis?.headers?.from?.value || "an unknown sender"; - const hasDkim = - report.dns_results?.dkim_records && report.dns_results?.dkim_records?.length > 0; - const dkimPassed = - report.authentication?.dkim && - report.authentication?.dkim.length > 0 && - report.authentication?.dkim?.some((d) => d.result === "pass"); + const hasDkim = report.authentication?.dkim && report.authentication?.dkim.length > 0; + const dkimPassed = hasDkim && report.authentication?.dkim?.some((d) => d.result === "pass"); segments.push({ text: "Received a " }); segments.push({ - text: hasDkim ? "DKIM-signed" : "non-DKIM-signed", - highlight: { - color: hasDkim ? (dkimPassed ? "good" : "warning") : "danger", - bold: true, - }, - link: hasDkim && dkimPassed ? "#authentication-dkim" : "#dns-details", + text: dkimPassed ? "DKIM-signed" : "non-DKIM-signed", + highlight: { color: dkimPassed ? "good" : "danger", bold: true }, + link: "#authentication-dkim", }); - segments.push({ text: " email" }); - if (hasDkim && !dkimPassed) { - segments.push({ text: " with " }); - segments.push({ - text: "an invalid signature", - highlight: { color: "danger", bold: true }, - link: "#authentication-dkim", - }); - } - segments.push({ text: " from " }); + segments.push({ text: " email from " }); segments.push({ text: mailFrom, highlight: { emphasis: true }, @@ -129,7 +113,7 @@ } else if (spfResult === "temperror" || spfResult === "permerror") { segments.push({ text: "encountered an error", - highlight: { color: "danger", bold: true }, + highlight: { color: "warning", bold: true }, link: "#authentication-spf", }); segments.push({ text: ", check your SPF record configuration" }); @@ -147,22 +131,14 @@ } // SPF DNS record check - const spfRecords = report.dns_results?.spf_records; - if (spfRecords && spfRecords.length > 0) { - const invalidSpfRecords = spfRecords.filter((r) => !r.valid && r.record); - if (invalidSpfRecords.length > 0) { - segments.push({ text: ". Your SPF record" }); - if (invalidSpfRecords.length > 1) { - segments.push({ text: "s are " }); - } else { - segments.push({ text: " is " }); - } - segments.push({ - text: "invalid", - highlight: { color: "danger", bold: true }, - link: "#dns-spf", - }); - } + const spfRecord = report.dns_results?.spf_record; + if (spfRecord && !spfRecord.valid && spfRecord.record) { + segments.push({ text: ". Your SPF record is " }); + segments.push({ + text: "invalid", + highlight: { color: "danger", bold: true }, + link: "#dns-spf", + }); } // IP Reverse DNS (iprev) check @@ -334,9 +310,7 @@ // BIMI const bimiResult = report.authentication?.bimi; if ( - dmarcRecord && - dmarcRecord.valid && - dmarcRecord.policy != "none" && + (dmarcRecord && dmarcRecord.valid && dmarcRecord.policy != "none") && (!bimiResult || bimiResult.result !== "skipped") ) { const bimiRecord = report.dns_results?.bimi_record; @@ -347,10 +321,10 @@ highlight: { color: "good", bold: true }, link: "#dns-bimi", }); - if (bimiResult?.details && bimiResult.details.indexOf("declined") == 0) { + if (bimiResult.details && bimiResult.details.indexOf("declined") == 0) { segments.push({ text: " declined to participate" }); - } else if (bimiResult?.result === "fail") { - segments.push({ text: " but " }); + } else if (bimiResult?.result !== "fail") { + segments.push({ text: " but" }); segments.push({ text: "has issues", highlight: { color: "danger", bold: true }, @@ -438,17 +412,6 @@ }); } - // One-click unsubscribe check - const unsubscribeMethods = report.content_analysis?.unsubscribe_methods; - if (unsubscribeMethods && unsubscribeMethods.length > 0 && !unsubscribeMethods.includes("one-click")) { - segments.push({ text: ". This email could benefit from " }); - segments.push({ - text: "one-click unsubscribe", - highlight: { color: "warning", bold: true }, - link: "#content-details", - }); - } - // Content/spam assessment const spamAssassin = report.spamassassin; const contentScore = report.summary?.content_score || 0; @@ -552,39 +515,19 @@ {#if segment.link} {segment.text} {:else if segment.highlight} - + {segment.text} {:else} {segment.text} {/if} {/each} - Overall, your email received a grade {#if report.grade == "A" || report.grade == "A+"}, well done 🎉{:else if report.grade == "C" || report.grade == "D"}: - you should try to increase your score to ensure inbox delivery.{:else if report.grade == "E"}: - you could have delivery issues with common providers.{:else if report.grade == "F"}: - it will most likely be rejected by most providers.{:else}!{/if} Check the details below - 🔽 + Overall, your email received a grade {#if report.grade == "A" || report.grade == "A+"}, well done 🎉{:else if report.grade == "C" || report.grade == "D"}: you should try to increase your score to ensure inbox delivery.{:else if report.grade == "E"}: you could have delivery issues with common providers.{:else if report.grade == "F"}: it will most likely be rejected by most providers.{:else}!{/if} Check the details below 🔽

{@render children?.()}
diff --git a/web/src/lib/components/WhitelistCard.svelte b/web/src/lib/components/WhitelistCard.svelte deleted file mode 100644 index 13fd86b..0000000 --- a/web/src/lib/components/WhitelistCard.svelte +++ /dev/null @@ -1,62 +0,0 @@ - - -
-
-

- - - Whitelist Checks - - Informational -

-
-
-

- DNS whitelists identify trusted senders. Being listed here is a positive signal, but has - no impact on the overall score. -

- -
- {#each Object.entries(whitelists) as [ip, checks]} -
-
- - {ip} -
- - - {#each checks as check} - - - - - {/each} - -
- - {check.error - ? "Error" - : check.listed - ? "Listed" - : "Not listed"} - - {check.rbl}
-
- {/each} -
-
-
diff --git a/web/src/lib/components/index.ts b/web/src/lib/components/index.ts index 8ed409c..dadab9e 100644 --- a/web/src/lib/components/index.ts +++ b/web/src/lib/components/index.ts @@ -1,27 +1,17 @@ // Component exports +export { default as FeatureCard } from "./FeatureCard.svelte"; +export { default as HowItWorksStep } from "./HowItWorksStep.svelte"; +export { default as ScoreCard } from "./ScoreCard.svelte"; +export { default as SummaryCard } from "./SummaryCard.svelte"; +export { default as SpamAssassinCard } from "./SpamAssassinCard.svelte"; +export { default as EmailAddressDisplay } from "./EmailAddressDisplay.svelte"; +export { default as PendingState } from "./PendingState.svelte"; export { default as AuthenticationCard } from "./AuthenticationCard.svelte"; -export { default as BimiRecordDisplay } from "./BimiRecordDisplay.svelte"; +export { default as DnsRecordsCard } from "./DnsRecordsCard.svelte"; export { default as BlacklistCard } from "./BlacklistCard.svelte"; export { default as ContentAnalysisCard } from "./ContentAnalysisCard.svelte"; -export { default as DkimRecordsDisplay } from "./DkimRecordsDisplay.svelte"; -export { default as DmarcRecordDisplay } from "./DmarcRecordDisplay.svelte"; -export { default as DnsRecordsCard } from "./DnsRecordsCard.svelte"; -export { default as EmailAddressDisplay } from "./EmailAddressDisplay.svelte"; -export { default as EmailPathCard } from "./EmailPathCard.svelte"; -export { default as ErrorDisplay } from "./ErrorDisplay.svelte"; -export { default as FeatureCard } from "./FeatureCard.svelte"; -export { default as GradeDisplay } from "./GradeDisplay.svelte"; export { default as HeaderAnalysisCard } from "./HeaderAnalysisCard.svelte"; -export { default as HowItWorksStep } from "./HowItWorksStep.svelte"; -export { default as Logo } from "./Logo.svelte"; -export { default as MxRecordsDisplay } from "./MxRecordsDisplay.svelte"; -export { default as PendingState } from "./PendingState.svelte"; -export { default as PtrForwardRecordsDisplay } from "./PtrForwardRecordsDisplay.svelte"; export { default as PtrRecordsDisplay } from "./PtrRecordsDisplay.svelte"; -export { default as ScoreCard } from "./ScoreCard.svelte"; -export { default as RspamdCard } from "./RspamdCard.svelte"; -export { default as SpamAssassinCard } from "./SpamAssassinCard.svelte"; -export { default as SpfRecordsDisplay } from "./SpfRecordsDisplay.svelte"; -export { default as SummaryCard } from "./SummaryCard.svelte"; +export { default as PtrForwardRecordsDisplay } from "./PtrForwardRecordsDisplay.svelte"; export { default as TinySurvey } from "./TinySurvey.svelte"; -export { default as WhitelistCard } from "./WhitelistCard.svelte"; +export { default as ErrorDisplay } from "./ErrorDisplay.svelte"; diff --git a/web/src/lib/stores/config.ts b/web/src/lib/stores/config.ts index c393dd2..4187307 100644 --- a/web/src/lib/stores/config.ts +++ b/web/src/lib/stores/config.ts @@ -24,14 +24,11 @@ import { writable } from "svelte/store"; interface AppConfig { report_retention?: number; survey_url?: string; - custom_logo_url?: string; - rbls?: string[]; } const defaultConfig: AppConfig = { report_retention: 0, survey_url: "", - rbls: [], }; function getConfigFromScriptTag(): AppConfig | null { diff --git a/web/src/lib/stores/theme.ts b/web/src/lib/stores/theme.ts index ea24293..f927276 100644 --- a/web/src/lib/stores/theme.ts +++ b/web/src/lib/stores/theme.ts @@ -1,32 +1,11 @@ -// This file is part of the happyDeliver (R) project. -// Copyright (c) 2025 happyDomain -// Authors: Pierre-Olivier Mercier, et al. -// -// This program is offered under a commercial and under the AGPL license. -// For commercial licensing, contact us at . -// -// For AGPL licensing: -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -import { browser } from "$app/environment"; import { writable } from "svelte/store"; +import { browser } from "$app/environment"; const getInitialTheme = () => { if (!browser) return "light"; const stored = localStorage.getItem("theme"); - if (stored === "light" || stored === "dark") return stored; + if (stored) return stored; return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"; }; diff --git a/web/src/routes/+error.svelte b/web/src/routes/+error.svelte index 0e103e5..a429ea5 100644 --- a/web/src/routes/+error.svelte +++ b/web/src/routes/+error.svelte @@ -1,9 +1,9 @@ - +