diff --git a/Dockerfile b/Dockerfile index 4568784..93ae993 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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,13 +165,7 @@ 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"] 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..23a189f 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -333,8 +333,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 +348,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 +401,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 @@ -789,7 +774,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, domain_pass, orgdomain_pass] description: Authentication result example: "pass" domain: @@ -858,17 +843,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 +900,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: @@ -1028,6 +942,10 @@ components: $ref: '#/components/schemas/DMARCRecord' bimi_record: $ref: '#/components/schemas/BIMIRecord' + dnssec_enabled: + type: boolean + description: Whether the From domain has DNSSEC enabled with valid chain of trust + example: true ptr_records: type: array items: @@ -1331,7 +1249,7 @@ components: type: object required: - ip - - blacklists + - checks - listed_count - score - grade @@ -1340,7 +1258,7 @@ components: type: string description: The IP address that was checked example: "192.0.2.1" - blacklists: + checks: type: array items: $ref: '#/components/schemas/BlacklistCheck' @@ -1360,8 +1278,3 @@ components: 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/docker-compose.yml b/docker-compose.yml index ccfd313..fa27c5c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -5,12 +5,12 @@ services: dockerfile: Dockerfile image: happydomain/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 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..85be917 100644 --- a/go.mod +++ b/go.mod @@ -1,15 +1,15 @@ 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.47.0 gorm.io/driver/postgres v1.6.0 gorm.io/driver/sqlite v1.6.0 gorm.io/gorm v1.31.1 @@ -18,25 +18,25 @@ require ( 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.2 // indirect + github.com/bytedance/sonic/loader v0.4.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.11 // 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.22.2 // indirect + github.com/go-openapi/swag/jsonname v0.25.1 // 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.28.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 @@ -46,7 +46,8 @@ require ( github.com/leodido/go-urn v1.4.0 // indirect github.com/mailru/easyjson v0.9.1 // 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/miekg/dns v1.1.4 // 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 @@ -55,24 +56,25 @@ require ( 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/peterzen/goresolver v1.0.2 // indirect + github.com/quic-go/qpack v0.5.1 // indirect + github.com/quic-go/quic-go v0.56.0 // indirect + github.com/redis/go-redis/v9 v9.16.0 // 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/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 + go.uber.org/mock v0.6.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 + golang.org/x/crypto v0.44.0 // indirect + golang.org/x/mod v0.30.0 // indirect + golang.org/x/sync v0.18.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect + golang.org/x/tools v0.39.0 // indirect + google.golang.org/protobuf v1.36.10 // 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..825604f 100644 --- a/go.sum +++ b/go.sum @@ -10,10 +10,10 @@ 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.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE= +github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980= +github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o= +github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= 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,18 +36,18 @@ 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.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik= +github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= 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/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.22.2 h1:JDQEe4B9j6K3tQ7HQQTZfjR59IURhjjLxet2FB4KHyg= +github.com/go-openapi/jsonpointer v0.22.2/go.mod h1:0lBbqeRsQ5lIanv3LHZBrmRGHLHcQoOXQnf88fHlGWo= +github.com/go-openapi/swag/jsonname v0.25.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU= +github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo= 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/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= @@ -56,15 +56,15 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o 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.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688= +github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU= 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 +90,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= @@ -118,8 +118,10 @@ 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/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/miekg/dns v1.1.4 h1:rCMZsU2ScVSYcAsOXgmC6+AKOK+6pmQTOcw03nfwYV0= +github.com/miekg/dns v1.1.4/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= 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= @@ -132,8 +134,8 @@ 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/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= @@ -154,14 +156,16 @@ github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0 github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s= github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= +github.com/peterzen/goresolver v1.0.2 h1:UxRxk835Onz7Go4oPUsOptSmBlIvN/yJ2kv3Srr3hw4= +github.com/peterzen/goresolver v1.0.2/go.mod h1:LrWRiOeCYApgvR2OhpipNOeaE1yGfI+QQjpF0riJC8M= 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.56.0 h1:q/TW+OLismmXAehgFLczhCDTYB3bFmua4D9lsNBWxvY= +github.com/quic-go/quic-go v0.56.0/go.mod h1:9gx5KsFQtw2oZ6GZTyh+7YEvOxWCL9WZAepnHxgAo6c= +github.com/redis/go-redis/v9 v9.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4= +github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= 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= @@ -194,36 +198,38 @@ github.com/vmware-labs/yaml-jsonpath v0.3.2/go.mod h1:U6whw1z03QyqgWdgXxvVnQ90zN github.com/woodsbury/decimal128 v1.4.0 h1:xJATj7lLu4f2oObouMt2tgGiElE5gO6mSWUjQsBgUlc= github.com/woodsbury/decimal128 v1.4.0/go.mod h1:BP46FUrVjVhdTbKT+XuQh2xfQaGki9LMIRJSFuh6THU= 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= +golang.org/x/crypto v0.0.0-20190222235706-ffb98f73852f/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 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.44.0 h1:A97SsFvM3AIwEEmTBiaxPPTYpDC47w720rdiiUvgoAU= +golang.org/x/crypto v0.44.0/go.mod h1:013i+Nw79BMiQiMsOPcVCB5ZIJbYkerPrGnOa00tvmc= 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.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/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= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 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.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/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.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I= +golang.org/x/sync v0.18.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-20190222171317-cd391775e71e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -237,21 +243,23 @@ 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.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.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.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= +golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 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.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= 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 +272,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.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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= diff --git a/internal/api/handlers.go b/internal/api/handlers.go index 470136e..80c8f9a 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -41,7 +41,7 @@ import ( 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) + CheckBlacklistIP(ip string) (checks []BlacklistCheck, listedCount int, score int, grade string, err error) } // APIHandler implements the ServerInterface for handling API requests @@ -359,7 +359,7 @@ func (h *APIHandler) CheckBlacklist(c *gin.Context) { } // Perform blacklist check using analyzer - checks, whitelists, listedCount, score, grade, err := h.analyzer.CheckBlacklistIP(request.Ip) + checks, listedCount, score, grade, err := h.analyzer.CheckBlacklistIP(request.Ip) if err != nil { c.JSON(http.StatusBadRequest, Error{ Error: "invalid_ip", @@ -372,8 +372,7 @@ func (h *APIHandler) CheckBlacklist(c *gin.Context) { // Build response response := BlacklistCheckResponse{ Ip: request.Ip, - Blacklists: checks, - Whitelists: &whitelists, + Checks: checks, ListedCount: listedCount, Score: score, Grade: BlacklistCheckResponseGrade(grade), 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/pkg/analyzer/analyzer.go b/pkg/analyzer/analyzer.go index f21d1f8..e7ae561 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{ @@ -123,28 +120,22 @@ func (a *APIAdapter) AnalyzeDomain(domain string) (*api.DNSResults, int, string) 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) { +// CheckBlacklistIP checks a single IP address against DNS blacklists +func (a *APIAdapter) CheckBlacklistIP(ip string) ([]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 + return nil, 0, 0, "", err } // Calculate score using the existing function // Create a minimal RBLResults structure for scoring - results := &DNSListResults{ + results := &RBLResults{ Checks: map[string][]api.BlacklistCheck{ip: checks}, IPsChecked: []string{ip}, ListedCount: listedCount, } - score, grade := a.analyzer.generator.rblChecker.CalculateScore(results, false) + score, grade := a.analyzer.generator.rblChecker.CalculateRBLScore(results) - // 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 + return checks, listedCount, score, grade, nil } diff --git a/pkg/analyzer/authentication.go b/pkg/analyzer/authentication.go index 2beeb1f..07f7794 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) } @@ -152,32 +150,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_test.go b/pkg/analyzer/authentication_dkim_test.go index 3218639..2aab530 100644 --- a/pkg/analyzer/authentication_dkim_test.go +++ b/pkg/analyzer/authentication_dkim_test.go @@ -58,7 +58,7 @@ func TestParseDKIMResult(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_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..95e32aa 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) @@ -737,7 +724,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 +870,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..9289d95 100644 --- a/pkg/analyzer/content_test.go +++ b/pkg/analyzer/content_test.go @@ -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..cb1fa68 100644 --- a/pkg/analyzer/dns.go +++ b/pkg/analyzer/dns.go @@ -54,7 +54,7 @@ func NewDNSAnalyzerWithResolver(timeout time.Duration, resolver DNSResolver) *DN } // 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 +104,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) } } @@ -122,6 +127,12 @@ func (d *DNSAnalyzer) AnalyzeDNS(email *EmailMessage, headersResults *api.Header // Check BIMI record (for From domain - branding is based on visible sender) results.BimiRecord = d.checkBIMIRecord(fromDomain, "default") + // Check DNSSEC status (for From domain) + dnssecEnabled, err := d.resolver.IsDNSSECEnabled(nil, fromDomain) + if err == nil { + results.DnssecEnabled = &dnssecEnabled + } + return results } @@ -144,6 +155,12 @@ func (d *DNSAnalyzer) AnalyzeDomainOnly(domain string) *api.DNSResults { // Check BIMI record with default selector results.BimiRecord = d.checkBIMIRecord(domain, "default") + // Check DNSSEC status + dnssecEnabled, err := d.resolver.IsDNSSECEnabled(nil, domain) + if err == nil { + results.DnssecEnabled = &dnssecEnabled + } + return results } @@ -199,11 +216,16 @@ func (d *DNSAnalyzer) CalculateDNSScore(results *api.DNSResults, senderIP string score := 0 + // DNSSEC: 10 points + if results.DnssecEnabled != nil && *results.DnssecEnabled { + score += 10 + } + // PTR and Forward DNS: 20 points score += 20 * d.calculatePTRScore(results, senderIP) / 100 - // MX Records: 20 points (10 for From domain, 10 for Return-Path domain) - score += 20 * d.calculateMXScore(results) / 100 + // MX Records: 10 points (5 for From domain, 5 for Return-Path domain) + score += 10 * d.calculateMXScore(results) / 100 // SPF Records: 20 points score += 20 * d.calculateSPFScore(results) / 100 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 index f60484f..dcbca59 100644 --- a/pkg/analyzer/dns_resolver.go +++ b/pkg/analyzer/dns_resolver.go @@ -23,7 +23,12 @@ package analyzer import ( "context" + "fmt" "net" + "strings" + + "github.com/miekg/dns" + "github.com/peterzen/goresolver" ) // DNSResolver defines the interface for DNS resolution operations. @@ -43,38 +48,190 @@ type DNSResolver interface { // 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) + + // IsDNSSECEnabled checks if the given domain has DNSSEC enabled by querying for DNSKEY records. + // Returns true if the domain has DNSSEC configured and the chain of trust is valid. + IsDNSSECEnabled(ctx context.Context, domain string) (bool, error) } -// StandardDNSResolver is the default DNS resolver implementation that uses net.Resolver. +// StandardDNSResolver is the default DNS resolver implementation that uses goresolver with DNSSEC validation. type StandardDNSResolver struct { - resolver *net.Resolver + resolver *goresolver.Resolver } -// NewStandardDNSResolver creates a new StandardDNSResolver with default settings. +// NewStandardDNSResolver creates a new StandardDNSResolver with DNSSEC validation support. func NewStandardDNSResolver() DNSResolver { + // Pass /etc/resolv.conf to load default DNS configuration + resolver, err := goresolver.NewResolver("/etc/resolv.conf") + if err != nil { + panic(fmt.Sprintf("failed to initialize goresolver: %v", err)) + } + return &StandardDNSResolver{ - resolver: &net.Resolver{ - PreferGo: true, - }, + resolver: resolver, } } -// LookupMX implements DNSResolver.LookupMX using net.Resolver. +// LookupMX implements DNSResolver.LookupMX using goresolver with DNSSEC validation. func (r *StandardDNSResolver) LookupMX(ctx context.Context, name string) ([]*net.MX, error) { - return r.resolver.LookupMX(ctx, name) + // Ensure the name ends with a dot for DNS queries + queryName := name + if !strings.HasSuffix(queryName, ".") { + queryName = queryName + "." + } + + rrs, err := r.resolver.StrictNSQuery(queryName, dns.TypeMX) + if err != nil { + return nil, err + } + + mxRecords := make([]*net.MX, 0, len(rrs)) + for _, rr := range rrs { + if mx, ok := rr.(*dns.MX); ok { + mxRecords = append(mxRecords, &net.MX{ + Host: strings.TrimSuffix(mx.Mx, "."), + Pref: mx.Preference, + }) + } + } + + if len(mxRecords) == 0 { + return nil, fmt.Errorf("no MX records found for %s", name) + } + + return mxRecords, nil } -// LookupTXT implements DNSResolver.LookupTXT using net.Resolver. +// LookupTXT implements DNSResolver.LookupTXT using goresolver with DNSSEC validation. func (r *StandardDNSResolver) LookupTXT(ctx context.Context, name string) ([]string, error) { - return r.resolver.LookupTXT(ctx, name) + // Ensure the name ends with a dot for DNS queries + queryName := name + if !strings.HasSuffix(queryName, ".") { + queryName = queryName + "." + } + + rrs, err := r.resolver.StrictNSQuery(queryName, dns.TypeTXT) + if err != nil { + return nil, err + } + + txtRecords := make([]string, 0, len(rrs)) + for _, rr := range rrs { + if txt, ok := rr.(*dns.TXT); ok { + // Join all TXT strings (a single TXT record can have multiple strings) + txtRecords = append(txtRecords, strings.Join(txt.Txt, "")) + } + } + + if len(txtRecords) == 0 { + return nil, fmt.Errorf("no TXT records found for %s", name) + } + + return txtRecords, nil } -// LookupAddr implements DNSResolver.LookupAddr using net.Resolver. +// LookupAddr implements DNSResolver.LookupAddr using goresolver with DNSSEC validation. func (r *StandardDNSResolver) LookupAddr(ctx context.Context, addr string) ([]string, error) { - return r.resolver.LookupAddr(ctx, addr) + // Convert IP address to reverse DNS name (e.g., 1.0.0.127.in-addr.arpa.) + arpa, err := dns.ReverseAddr(addr) + if err != nil { + return nil, fmt.Errorf("invalid IP address: %w", err) + } + + rrs, err := r.resolver.StrictNSQuery(arpa, dns.TypePTR) + if err != nil { + return nil, err + } + + ptrRecords := make([]string, 0, len(rrs)) + for _, rr := range rrs { + if ptr, ok := rr.(*dns.PTR); ok { + ptrRecords = append(ptrRecords, strings.TrimSuffix(ptr.Ptr, ".")) + } + } + + if len(ptrRecords) == 0 { + return nil, fmt.Errorf("no PTR records found for %s", addr) + } + + return ptrRecords, nil } -// LookupHost implements DNSResolver.LookupHost using net.Resolver. +// LookupHost implements DNSResolver.LookupHost using goresolver with DNSSEC validation. func (r *StandardDNSResolver) LookupHost(ctx context.Context, host string) ([]string, error) { - return r.resolver.LookupHost(ctx, host) + // Ensure the host ends with a dot for DNS queries + queryName := host + if !strings.HasSuffix(queryName, ".") { + queryName = queryName + "." + } + + var allAddrs []string + + // Query A records (IPv4) + rrsA, errA := r.resolver.StrictNSQuery(queryName, dns.TypeA) + if errA == nil { + for _, rr := range rrsA { + if a, ok := rr.(*dns.A); ok { + allAddrs = append(allAddrs, a.A.String()) + } + } + } + + // Query AAAA records (IPv6) + rrsAAAA, errAAAA := r.resolver.StrictNSQuery(queryName, dns.TypeAAAA) + if errAAAA == nil { + for _, rr := range rrsAAAA { + if aaaa, ok := rr.(*dns.AAAA); ok { + allAddrs = append(allAddrs, aaaa.AAAA.String()) + } + } + } + + // Return error only if both queries failed + if errA != nil && errAAAA != nil { + return nil, fmt.Errorf("failed to resolve host: IPv4 error: %v, IPv6 error: %v", errA, errAAAA) + } + + if len(allAddrs) == 0 { + return nil, fmt.Errorf("no A or AAAA records found for %s", host) + } + + return allAddrs, nil +} + +// IsDNSSECEnabled checks if the given domain has DNSSEC enabled by querying for DNSKEY records. +// It uses DNSSEC validation to ensure the chain of trust is valid. +// Returns true if DNSSEC is properly configured and validated, false otherwise. +func (r *StandardDNSResolver) IsDNSSECEnabled(ctx context.Context, domain string) (bool, error) { + // Ensure the domain ends with a dot for DNS queries + queryName := domain + if !strings.HasSuffix(queryName, ".") { + queryName = queryName + "." + } + + // Query for DNSKEY records with DNSSEC validation + // If this succeeds, it means: + // 1. The domain has DNSKEY records (DNSSEC is configured) + // 2. The DNSSEC chain of trust is valid (validated by StrictNSQuery) + rrs, err := r.resolver.StrictNSQuery(queryName, dns.TypeDNSKEY) + if err != nil { + // DNSSEC is not enabled or validation failed + return false, nil + } + + // Check if we got any DNSKEY records + if len(rrs) == 0 { + return false, nil + } + + // Verify we actually have DNSKEY records (not just any RR type) + hasDNSKEY := false + for _, rr := range rrs { + if _, ok := rr.(*dns.DNSKEY); ok { + hasDNSKEY = true + break + } + } + + return hasDNSKEY, nil } diff --git a/pkg/analyzer/dns_resolver_test.go b/pkg/analyzer/dns_resolver_test.go new file mode 100644 index 0000000..7c9091b --- /dev/null +++ b/pkg/analyzer/dns_resolver_test.go @@ -0,0 +1,111 @@ +// 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" + "testing" +) + +func TestIsDNSSECEnabled(t *testing.T) { + resolver := NewStandardDNSResolver() + ctx := context.Background() + + tests := []struct { + name string + domain string + expectDNSSEC bool + }{ + { + name: "ietf.org has DNSSEC", + domain: "ietf.org", + expectDNSSEC: true, + }, + { + name: "google.com doesn't have DNSSEC", + domain: "google.com", + expectDNSSEC: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + enabled, err := resolver.IsDNSSECEnabled(ctx, tt.domain) + if err != nil { + t.Errorf("IsDNSSECEnabled() error = %v", err) + return + } + + if enabled != tt.expectDNSSEC { + t.Errorf("IsDNSSECEnabled() for %s = %v, want %v", tt.domain, enabled, tt.expectDNSSEC) + } else { + // Log the result even if we're not validating + if enabled { + t.Logf("%s: DNSSEC is enabled ✅", tt.domain) + } else { + t.Logf("%s: DNSSEC is NOT enabled ⚠️", tt.domain) + } + } + }) + } +} + +func TestIsDNSSECEnabled_NonExistentDomain(t *testing.T) { + resolver := NewStandardDNSResolver() + ctx := context.Background() + + // Test with a domain that doesn't exist + enabled, err := resolver.IsDNSSECEnabled(ctx, "this-domain-definitely-does-not-exist-12345.com") + if err != nil { + // Error is acceptable for non-existent domains + t.Logf("Non-existent domain returned error (expected): %v", err) + return + } + + // If no error, DNSSEC should be disabled + if enabled { + t.Error("IsDNSSECEnabled() for non-existent domain should return false") + } +} + +func TestIsDNSSECEnabled_WithTrailingDot(t *testing.T) { + resolver := NewStandardDNSResolver() + ctx := context.Background() + + // Test that both formats work + domain1 := "cloudflare.com" + domain2 := "cloudflare.com." + + enabled1, err1 := resolver.IsDNSSECEnabled(ctx, domain1) + if err1 != nil { + t.Errorf("IsDNSSECEnabled() without trailing dot error = %v", err1) + } + + enabled2, err2 := resolver.IsDNSSECEnabled(ctx, domain2) + if err2 != nil { + t.Errorf("IsDNSSECEnabled() with trailing dot error = %v", err2) + } + + if enabled1 != enabled2 { + t.Errorf("IsDNSSECEnabled() results differ: without dot = %v, with dot = %v", enabled1, enabled2) + } +} diff --git a/pkg/analyzer/headers.go b/pkg/analyzer/headers.go index 37718bb..b7ff3bb 100644 --- a/pkg/analyzer/headers.go +++ b/pkg/analyzer/headers.go @@ -109,13 +109,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 @@ -273,10 +266,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 { @@ -331,21 +320,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 { 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..eb1fc6a 100644 --- a/pkg/analyzer/parser_test.go +++ b/pkg/analyzer/parser_test.go @@ -106,6 +106,9 @@ Content-Type: text/html; charset=utf-8 } func TestGetAuthenticationResults(t *testing.T) { + // Force hostname + hostname = "example.com" + rawEmail := `From: sender@example.com To: recipient@example.com Subject: Test Email @@ -120,7 +123,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..5fcb939 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,26 +108,20 @@ 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) { +// CheckIP checks a single IP address against all configured RBLs +func (r *RBLChecker) CheckIP(ip string) ([]api.BlacklistCheck, int, error) { + // Validate that it's a valid IP address 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() - + var checks []api.BlacklistCheck listedCount := 0 - for _, check := range checks { + + // Check the IP against all RBLs + for _, rbl := range r.RBLs { + check := r.checkIP(ip, rbl) + checks = append(checks, check) if check.Listed { listedCount++ } @@ -185,19 +131,27 @@ func (r *DNSListChecker) CheckIP(ip string) ([]api.BlacklistCheck, int, error) { } // 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 +159,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 +178,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 +198,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 +250,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 +295,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..39871fe 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 @@ -85,11 +76,9 @@ 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.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..0a23388 100644 --- a/pkg/analyzer/scoring.go +++ b/pkg/analyzer/scoring.go @@ -69,33 +69,3 @@ func ScoreToGradeKind(score int) string { 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..4ac32c2 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", + "@eslint/compat": "^1.4.0", + "@eslint/js": "^9.36.0", "@hey-api/openapi-ts": "0.86.10", "@sveltejs/adapter-static": "^3.0.9", "@sveltejs/kit": "^2.43.2", - "@sveltejs/vite-plugin-svelte": "^7.0.0", + "@sveltejs/vite-plugin-svelte": "^6.2.0", "@types/node": "^24.0.0", - "eslint": "^10.0.0", + "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.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "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.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "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.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "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.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "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.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "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.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "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.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "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.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "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.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "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.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "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.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "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.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "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.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "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.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "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.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "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.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "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.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "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.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", "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.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "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.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", "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.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "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.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", "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.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "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.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "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.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "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.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "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.1", + "resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-1.4.1.tgz", + "integrity": "sha512-cfO82V9zxxGBxcQDr1lfaYB7wykTa0b00mGa36FrJl7iTFd0Z2cHfEYuxcBRP/iNijCsWsEkA+jzT8hGYmv33w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.1" + "@eslint/core": "^0.17.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,89 +540,118 @@ } }, "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.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.1" + "@eslint/core": "^0.17.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.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", "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.39.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz", + "integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==", + "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.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.1", + "@eslint/core": "^0.17.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": { @@ -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.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.2.tgz", + "integrity": "sha512-yDPzwsgiFO26RJA4nZo8I+xqzh7sJTZIWQOxn+/XOdPE31lAvLIYCKqjV+lNH/vxE2L2iH3plKxDCRK6i+CwhA==", "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.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.2.tgz", + "integrity": "sha512-k8FontTxIE7b0/OGKeSN5B6j25EuppBcWM33Z19JoVT7UTXFSo3D9CdU39wGTeb29NO3XxpMNauh09B+Ibw+9g==", "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.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.2.tgz", + "integrity": "sha512-A6s4gJpomNBtJ2yioj8bflM2oogDwzUiMl2yNJ2v9E7++sHrSrsQ29fOfn5DM/iCzpWcebNYEdXpaK4tr2RhfQ==", "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.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.2.tgz", + "integrity": "sha512-e6XqVmXlHrBlG56obu9gDRPW3O3hLxpwHpLsBJvuI8qqnsrtSZ9ERoWUXtPOkY8c78WghyPHZdmPhHLWNdAGEw==", "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.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.2.tgz", + "integrity": "sha512-v0E9lJW8VsrwPux5Qe5CwmH/CF/2mQs6xU1MF3nmUxmZUCHazCjLgYvToOk+YuuUqLQBio1qkkREhxhc656ViA==", "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.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.2.tgz", + "integrity": "sha512-ClAmAPx3ZCHtp6ysl4XEhWU69GUB1D+s7G9YjHGhIGCSrsg00nEGRRZHmINYxkdoJehde8VIsDC5t9C0gb6yqA==", "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.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.2.tgz", + "integrity": "sha512-EPlb95nUsz6Dd9Qy13fI5kUPXNSljaG9FiJ4YUGU1O/Q77i5DYFW5KR8g1OzTcdZUqQQ1KdDqsTohdFVwCwjqg==", "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.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.2.tgz", + "integrity": "sha512-BOmnVW+khAUX+YZvNfa0tGTEMVVEerOxN0pDk2E6N6DsEIa2Ctj48FOMfNDdrwinocKaC7YXUZ1pHlKpnkja/Q==", "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.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.2.tgz", + "integrity": "sha512-Xt2byDZ+6OVNuREgBXr4+CZDJtrVso5woFtpKdGPhpTPHcNG7D8YXeQzpNbFRxzTVqJf7kvPMCub/pcGUWgBjA==", "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.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.2.tgz", + "integrity": "sha512-+LdZSldy/I9N8+klim/Y1HsKbJ3BbInHav5qE9Iy77dtHC/pibw1SR/fXlWyAk0ThnpRKoODwnAuSjqxFRDHUQ==", "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.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.2.tgz", + "integrity": "sha512-8ms8sjmyc1jWJS6WdNSA23rEfdjWB30LH8Wqj0Cqvv7qSHnvw6kgMMXRdop6hkmGPlyYBdRPkjJnj3KCUHV/uQ==", "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.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.2.tgz", + "integrity": "sha512-3HRQLUQbpBDMmzoxPJYd3W6vrVHOo2cVW8RUo87Xz0JPJcBLBr5kZ1pGcQAhdZgX9VV7NbGNipah1omKKe23/g==", "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.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.2.tgz", + "integrity": "sha512-fMjKi+ojnmIvhk34gZP94vjogXNNUKMEYs+EDaB/5TG/wUkoeua7p7VCHnE6T2Tx+iaghAqQX8teQzcvrYpaQA==", "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.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.2.tgz", + "integrity": "sha512-XuGFGU+VwUUV5kLvoAdi0Wz5Xbh2SrjIxCtZj6Wq8MDp4bflb/+ThZsVxokM7n0pcbkEr2h5/pzqzDYI7cCgLQ==", "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.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.2.tgz", + "integrity": "sha512-w6yjZF0P+NGzWR3AXWX9zc0DNEGdtvykB03uhonSHMRa+oWA6novflo2WaJr6JZakG2ucsyb+rvhrKac6NIy+w==", "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.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.2.tgz", + "integrity": "sha512-yo8d6tdfdeBArzC7T/PnHd7OypfI9cbuZzPnzLJIyKYFhAQ8SvlkKtKBMbXDxe1h03Rcr7u++nFS7tqXz87Gtw==", "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.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.2.tgz", + "integrity": "sha512-ah59c1YkCxKExPP8O9PwOvs+XRLKwh/mV+3YdKqQ5AMQ0r4M4ZDuOrpWkUaqO7fzAHdINzV9tEVu8vNw48z0lA==", "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.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.2.tgz", + "integrity": "sha512-4VEd19Wmhr+Zy7hbUsFZ6YXEiP48hE//KPLCSVNY5RMGX2/7HZ+QkN55a3atM1C/BZCGIgqN+xrVgtdak2S9+A==", "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.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.2.tgz", + "integrity": "sha512-IlbHFYc/pQCgew/d5fslcy1KEaYVCJ44G8pajugd8VoOEI8ODhtb/j8XMhLpwHCMB3yk2J07ctup10gpw2nyMA==", "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.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.2.tgz", + "integrity": "sha512-lNlPEGgdUfSzdCWU176ku/dQRnA7W+Gp8d+cWv73jYrb8uT7HTVVxq62DUYxjbaByuf1Yk0RIIAbDzp+CnOTFg==", "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.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.2.tgz", + "integrity": "sha512-S6YojNVrHybQis2lYov1sd+uj7K0Q05NxHcGktuMMdIQ2VixGwAfbJ23NnlvvVV1bdpR2m5MsNBViHJKcA4ADw==", "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.53.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.2.tgz", + "integrity": "sha512-k+/Rkcyx//P6fetPoLMb8pBeqJBNGx81uuf7iljX9++yNBVRDQgD04L+SVXmXmh5ZP4/WOp4mWF0kmi06PW2tA==", "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.5", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.48.5.tgz", + "integrity": "sha512-/rnwfSWS3qwUSzvHynUTORF9xSJi7PCR9yXkxUOnRrNqyKmCmh3FPHH+E9BbgqxXfTevGXBqgnlh9kMb+9T5XA==", "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": "24.10.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", + "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.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.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.4.tgz", + "integrity": "sha512-R48VhmTJqplNyDxCyqqVkFSZIx1qX6PzwqgcXn1olLrzxcSBDlOsbtcnQuQhNtnNiJ4Xe5gREI1foajYaYU2Vg==", "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.4", + "@typescript-eslint/type-utils": "8.46.4", + "@typescript-eslint/utils": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", + "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.4", + "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.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.46.4.tgz", + "integrity": "sha512-tK3GPFWbirvNgsNKto+UmB/cRtn6TZfyw0D6IKrW55n6Vbs7KJoZtI//kpTKzE/DUmmnAFD8/Ca46s7Obs92/w==", "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.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", + "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.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.46.4.tgz", + "integrity": "sha512-nPiRSKuvtTN+no/2N1kt2tUh/HoFzeEgOm9fQ6XQk4/ApGqjx0zFIIaLJ6wooR1HIoozvj2j6vTi/1fgAz7UYQ==", "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.4", + "@typescript-eslint/types": "^8.46.4", + "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.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.4.tgz", + "integrity": "sha512-tMDbLGXb1wC+McN1M6QeDx7P7c0UWO5z9CXqp7J8E+xGcJuUuevWKxuG8j41FoweS3+L41SkyKKkia16jpX7CA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1" + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4" }, "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.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.4.tgz", + "integrity": "sha512-+/XqaZPIAk6Cjg7NWgSGe27X4zMGqrFqZ8atJsX3CWxH/jACqWnrWI68h7nHQld0y+k9eTTjb9r+KU4twLoo9A==", "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.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.4.tgz", + "integrity": "sha512-V4QC8h3fdT5Wro6vANk6eojqfbv5bpwHuMsBcJUJkqs2z5XnYhJzyz9Y02eUmF9u3PgXEUiOt4w4KHR3P+z0PQ==", "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.4", + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/utils": "8.46.4", + "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.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.46.4.tgz", + "integrity": "sha512-USjyxm3gQEePdUwJBFjjGNG18xY9A2grDVGuk7/9AkjIF1L+ZrVnwR5VAU5JXtUnBL/Nwt3H31KlRDaksnM7/w==", "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.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.4.tgz", + "integrity": "sha512-7oV2qEOr1d4NWNmpXLR35LvCfOkTNymY9oyW+lUHkmCno7aOmIf/hMaydnJBUTBMRCOGZh8YjkFOc8dadEoNGA==", "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.4", + "@typescript-eslint/tsconfig-utils": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/visitor-keys": "8.46.4", + "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.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.46.4.tgz", + "integrity": "sha512-AbSv11fklGXV6T28dp2Me04Uw90R2iJ30g2bgLz529Koehrmkbs1r7paFqr1vPCZi7hHwYxYtxfyQMRC8QaVSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.46.4", + "@typescript-eslint/types": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4" }, "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.4", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.4.tgz", + "integrity": "sha512-/++5CYLQqsO9HFGLI7APrxBJYo+5OCMpViuhV8q5/Qa3o5mMrF//eQHks+PXcsAVaLdn817fMuS7zqoXNNZGaw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.57.1", - "eslint-visitor-keys": "^5.0.0" + "@typescript-eslint/types": "8.46.4", + "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.3.0", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.3.0.tgz", + "integrity": "sha512-Qq68+VkJlc8tjnPV1i7HtbIn7ohmjZa88qUvHMIK0ZKUXMCuV45cT7cEXALPUmeXCe0q1DWQkQTemHVaLIFSrg==", "dev": true, "license": "MIT", "dependencies": { @@ -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.5.0", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.5.0.tgz", + "integrity": "sha512-69sM5yrHfFLJt0AZ9QqZXGCPfJ7fQjvpln3Rq5+PS03LD32Ost1Q9N+eEnaQwGRIriKkMImXD56ocjQmfjbV3w==", "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.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "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.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "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.39.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz", + "integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==", "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.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.39.1", + "@eslint/plugin-kit": "^0.4.1", "@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.13.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.13.0.tgz", + "integrity": "sha512-2ohCCQJJTNbIpQCSDSTWj+FN0OVfPmSO03lmSNT7ytqMaWF6kpT86LdzDqtm4sh7TVPl/OEWJ/d7R87bXP2Vjg==", "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.2", + "resolved": "https://registry.npmjs.org/esrap/-/esrap-2.1.2.tgz", + "integrity": "sha512-DgvlIQeowRNyvLPWW4PT7Gu13WznY288Du086E751mwwbsgr29ytBiYeLzAGIo0qk3Ujob0SDk8TiSaM5WQzNg==", "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": { @@ -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.5.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz", + "integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==", "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": { @@ -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,41 @@ "@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/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" }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "engines": { + "node": "*" } }, "node_modules/minimist": { @@ -3562,41 +3223,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 +3318,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 +3369,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" }, @@ -3725,13 +3383,13 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", - "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "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": ">=12" + "node": ">=8.6" }, "funding": { "url": "https://github.com/sponsors/jonschlinkert" @@ -3750,9 +3408,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 +3427,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", @@ -3809,9 +3468,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 +3532,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 +3556,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 +3573,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 +3593,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 +3639,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.53.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.2.tgz", + "integrity": "sha512-MHngMYwGJVi6Fmnk6ISmnk7JAHRNF0UkuucA0CUW3N3a4KnONPEZz+vUanQP/ZC/iY1Qkf3bwPWzyY84wEks1g==", "dev": true, "license": "MIT", "dependencies": { @@ -4009,31 +3677,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.53.2", + "@rollup/rollup-android-arm64": "4.53.2", + "@rollup/rollup-darwin-arm64": "4.53.2", + "@rollup/rollup-darwin-x64": "4.53.2", + "@rollup/rollup-freebsd-arm64": "4.53.2", + "@rollup/rollup-freebsd-x64": "4.53.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.53.2", + "@rollup/rollup-linux-arm-musleabihf": "4.53.2", + "@rollup/rollup-linux-arm64-gnu": "4.53.2", + "@rollup/rollup-linux-arm64-musl": "4.53.2", + "@rollup/rollup-linux-loong64-gnu": "4.53.2", + "@rollup/rollup-linux-ppc64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-gnu": "4.53.2", + "@rollup/rollup-linux-riscv64-musl": "4.53.2", + "@rollup/rollup-linux-s390x-gnu": "4.53.2", + "@rollup/rollup-linux-x64-gnu": "4.53.2", + "@rollup/rollup-linux-x64-musl": "4.53.2", + "@rollup/rollup-openharmony-arm64": "4.53.2", + "@rollup/rollup-win32-arm64-msvc": "4.53.2", + "@rollup/rollup-win32-ia32-msvc": "4.53.2", + "@rollup/rollup-win32-x64-gnu": "4.53.2", + "@rollup/rollup-win32-x64-msvc": "4.53.2", "fsevents": "~2.3.2" } }, @@ -4050,6 +3715,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 +3766,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.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", "dev": true, "license": "MIT" }, @@ -4162,6 +3851,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 +3877,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.43.6", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-5.43.6.tgz", + "integrity": "sha512-RnyO9VXI85Bmsf4b8AuQFBKFYL3LKUl+ZrifOjvlrQoboAROj5IITVLK1yOXBjwUWUn2BI5cfmurktgCzuZ5QA==", + "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 +3918,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.4", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.4.tgz", + "integrity": "sha512-DVWvxhBrDsd+0hHWKfjP99lsSXASeOhHJYyuKOFYJcP7ThfSCKgjVarE8XfuMWpS5JV3AlDf+iK1YGGo2TACdw==", "dev": true, "license": "MIT", "dependencies": { @@ -4228,9 +3942,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 +3953,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 +3971,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,9 +3979,9 @@ "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.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", "dev": true, "license": "MIT", "engines": { @@ -4340,6 +4005,19 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/tinypool": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", @@ -4370,6 +4048,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 +4072,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 +4084,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 +4103,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -4429,16 +4113,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.4", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.46.4.tgz", + "integrity": "sha512-KALyxkpYV5Ix7UhvjTwJXZv76VWsHG+NjNlt/z+a17SOQSiOcBdUXdbJdyXi7RPxrBFECtFOiPwUJQusJuCqrg==", "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.4", + "@typescript-eslint/parser": "8.46.4", + "@typescript-eslint/typescript-estree": "8.46.4", + "@typescript-eslint/utils": "8.46.4" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -4448,7 +4132,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" } }, @@ -4491,114 +4175,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.2.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.2.tgz", + "integrity": "sha512-BxAKBWmIbrDgrokdGZH1IgkIk/5mMHDreLDmCJ0qpyJaAteP8NvMhkwr/ZCQNqNH97bw/dANTE9PDzqwJghfMQ==", "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 +4250,46 @@ } } }, + "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_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "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 +4298,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,31 +4379,17 @@ } } }, - "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==", + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "dependencies": { - "@vitest/spy": "3.2.4", - "estree-walker": "^3.0.3", - "magic-string": "^0.30.17" + "engines": { + "node": ">=12" }, "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 - } + "url": "https://github.com/sponsors/jonschlinkert" } }, "node_modules/vitest/node_modules/tinyexec": { @@ -4793,81 +4399,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", @@ -4935,19 +4466,18 @@ } }, "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, + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "dev": true, "license": "ISC", + "optional": true, + "peer": true, "bin": { "yaml": "bin.mjs" }, "engines": { "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" } }, "node_modules/yocto-queue": { diff --git a/web/package.json b/web/package.json index fce3e61..c1efabe 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", + "@eslint/compat": "^1.4.0", + "@eslint/js": "^9.36.0", "@hey-api/openapi-ts": "0.86.10", "@sveltejs/adapter-static": "^3.0.9", "@sveltejs/kit": "^2.43.2", - "@sveltejs/vite-plugin-svelte": "^7.0.0", + "@sveltejs/vite-plugin-svelte": "^6.2.0", "@types/node": "^24.0.0", - "eslint": "^10.0.0", + "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..23a9bbb 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" @@ -66,10 +67,6 @@ func DeclareRoutes(cfg *config.Config, router *gin.Engine) { 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 { @@ -143,7 +140,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 +167,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..8f22eac 100644 --- a/web/src/lib/components/AuthenticationCard.svelte +++ b/web/src/lib/components/AuthenticationCard.svelte @@ -13,19 +13,12 @@ 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": @@ -58,7 +51,6 @@ case "neutral": case "invalid": case "null": - case "permerror": case "error": case "null_smtp": case "null_header": @@ -103,465 +95,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/DnsRecordsCard.svelte b/web/src/lib/components/DnsRecordsCard.svelte index b7997b0..1bf02f7 100644 --- a/web/src/lib/components/DnsRecordsCard.svelte +++ b/web/src/lib/components/DnsRecordsCard.svelte @@ -1,15 +1,16 @@ + +{#if dnssecEnabled !== undefined} +
+
+
+ + DNSSEC +
+ Security +
+
+

+ DNSSEC (DNS Security Extensions) adds cryptographic signatures to DNS records to verify + their authenticity and integrity. It protects against DNS spoofing and cache poisoning + attacks, ensuring that DNS responses haven't been tampered with. +

+ {#if domain} +
+ Domain: {domain} +
+ {/if} + {#if dnssecIsValid} +
+ + Enabled: DNSSEC is properly configured with a valid chain of trust. + This provides additional security and authenticity for your domain's DNS records. +
+ {:else} +
+ + Not Enabled: DNSSEC is not configured for this domain. While not + required for email delivery, enabling DNSSEC provides additional security by protecting + against DNS-based attacks. Consider enabling DNSSEC through your domain registrar or + DNS provider. +
+ {/if} +
+
+{/if} diff --git a/web/src/lib/components/EmailPathCard.svelte b/web/src/lib/components/EmailPathCard.svelte index a4fda45..422ba0a 100644 --- a/web/src/lib/components/EmailPathCard.svelte +++ b/web/src/lib/components/EmailPathCard.svelte @@ -1,6 +1,5 @@ {#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..e0ecb58 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..8817004 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" }); @@ -334,9 +318,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,7 +329,7 @@ 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 " }); @@ -438,17 +420,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 +523,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..fd4f3c9 100644 --- a/web/src/lib/components/index.ts +++ b/web/src/lib/components/index.ts @@ -1,27 +1,25 @@ // 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"; +export { default as GradeDisplay } from "./GradeDisplay.svelte"; +export { default as MxRecordsDisplay } from "./MxRecordsDisplay.svelte"; +export { default as SpfRecordsDisplay } from "./SpfRecordsDisplay.svelte"; +export { default as DmarcRecordDisplay } from "./DmarcRecordDisplay.svelte"; +export { default as BimiRecordDisplay } from "./BimiRecordDisplay.svelte"; +export { default as DkimRecordsDisplay } from "./DkimRecordsDisplay.svelte"; +export { default as Logo } from "./Logo.svelte"; +export { default as EmailPathCard } from "./EmailPathCard.svelte"; diff --git a/web/src/lib/stores/config.ts b/web/src/lib/stores/config.ts index c393dd2..8a978e0 100644 --- a/web/src/lib/stores/config.ts +++ b/web/src/lib/stores/config.ts @@ -24,8 +24,6 @@ import { writable } from "svelte/store"; interface AppConfig { report_retention?: number; survey_url?: string; - custom_logo_url?: string; - rbls?: string[]; } const defaultConfig: AppConfig = { 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 @@ - +