Compare commits
3 commits
master
...
f/unsubscr
| Author | SHA1 | Date | |
|---|---|---|---|
| 43aec8fdc0 | |||
| 1c1d474870 | |||
| 521d5da84c |
43 changed files with 475 additions and 1807 deletions
14
Dockerfile
14
Dockerfile
|
|
@ -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,12 +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
|
||||
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"]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
@ -163,7 +162,7 @@ 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.
|
||||
|
||||
Choose one of the following way to integrate happyDeliver in your existing setup:
|
||||
|
|
@ -270,7 +269,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
|
||||
|
|
|
|||
114
api/openapi.yaml
114
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
|
||||
|
|
@ -931,81 +905,6 @@ components:
|
|||
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/RspamdSymbol'
|
||||
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)
|
||||
|
||||
RspamdSymbol:
|
||||
type: object
|
||||
required:
|
||||
- name
|
||||
- score
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: Symbol name
|
||||
example: "BAYES_HAM"
|
||||
score:
|
||||
type: number
|
||||
format: float
|
||||
description: Score contribution of this symbol
|
||||
example: -1.9
|
||||
params:
|
||||
type: string
|
||||
description: Symbol parameters or options
|
||||
example: "0.02"
|
||||
|
||||
DNSResults:
|
||||
type: object
|
||||
required:
|
||||
|
|
@ -1346,7 +1245,7 @@ components:
|
|||
type: object
|
||||
required:
|
||||
- ip
|
||||
- blacklists
|
||||
- checks
|
||||
- listed_count
|
||||
- score
|
||||
- grade
|
||||
|
|
@ -1355,7 +1254,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'
|
||||
|
|
@ -1375,8 +1274,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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,5 +0,0 @@
|
|||
no_action = 0;
|
||||
reject = null;
|
||||
add_header = null;
|
||||
rewrite_subject = null;
|
||||
greylist = null;
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
# Add "extended Rspamd headers"
|
||||
extended_spam_headers = true;
|
||||
|
||||
skip_local = false;
|
||||
skip_authenticated = false;
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
# rspamd options for happyDeliver
|
||||
# Disable Bayes learning to keep the setup stateless
|
||||
use_redis = false;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
|
|||
12
go.mod
12
go.mod
|
|
@ -9,7 +9,7 @@ require (
|
|||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/oapi-codegen/runtime v1.1.2
|
||||
golang.org/x/net v0.50.0
|
||||
golang.org/x/net v0.49.0
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
|
|
@ -66,12 +66,12 @@ require (
|
|||
github.com/woodsbury/decimal128 v1.4.0 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
golang.org/x/arch v0.23.0 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/mod v0.32.0 // indirect
|
||||
golang.org/x/crypto v0.47.0 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
golang.org/x/tools v0.41.0 // indirect
|
||||
golang.org/x/sys v0.40.0 // indirect
|
||||
golang.org/x/text v0.33.0 // indirect
|
||||
golang.org/x/tools v0.40.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
|
|
|||
51
go.sum
51
go.sum
|
|
@ -10,8 +10,12 @@ 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.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
|
||||
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
|
||||
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.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
|
||||
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
|
|
@ -36,6 +40,8 @@ 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.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
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/getkin/kin-openapi v0.133.0 h1:pJdmNohVIJ97r4AUFtEXRXwESr8b0bD721u/Tz6k8PQ=
|
||||
|
|
@ -44,8 +50,12 @@ 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.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/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.1 h1:Sgx+qbwa4ej6AomWC6pEfXrA6uP2RkaNjA9BR8a1RJU=
|
||||
github.com/go-openapi/swag/jsonname v0.25.1/go.mod h1:71Tekow6UOLBD3wS7XhdT98g5J5GR13NOTQ9/6Q11Zo=
|
||||
github.com/go-openapi/swag/jsonname v0.25.4 h1:bZH0+MsS03MbnwBXYhuTttMOqk+5KcQ9869Vye1bNHI=
|
||||
github.com/go-openapi/swag/jsonname v0.25.4/go.mod h1:GPVEk9CWVhNvWhZgrnvRA6utbAltopbKwDu8mXNUMag=
|
||||
github.com/go-openapi/testify/v2 v2.0.2 h1:X999g3jeLcoY8qctY/c/Z8iBHTbwLz7R2WXd6Ub6wls=
|
||||
|
|
@ -56,6 +66,8 @@ 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.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||
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-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
|
||||
|
|
@ -63,6 +75,8 @@ 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.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
|
|
@ -90,6 +104,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.7.6 h1:rWQc5FwZSPX58r1OQmkuaNicxdmExaEz5A2DO2hUuTk=
|
||||
github.com/jackc/pgx/v5 v5.7.6/go.mod h1:aruU7o91Tc2q2cFp5h4uP3f6ztExVpyVv88Xl/8Vl8M=
|
||||
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/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
|
|
@ -118,6 +134,8 @@ 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.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
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/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
|
|
@ -158,8 +176,12 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||
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.57.0 h1:AsSSrrMs4qI/hLrKlTH/TGQeTMY0ib1pAOX7vA3AdqE=
|
||||
github.com/quic-go/quic-go v0.57.0/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
|
||||
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.16.0 h1:OotgqgLSRCmzfqChbQyG1PHC3tLNR89DG4jdOERSEP4=
|
||||
github.com/redis/go-redis/v9 v9.16.0/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||
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/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
|
|
@ -201,11 +223,11 @@ golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
|||
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.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
|
||||
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=
|
||||
golang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=
|
||||
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
|
||||
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
|
|
@ -213,8 +235,8 @@ golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/
|
|||
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=
|
||||
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
|
@ -235,21 +257,24 @@ 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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
|
||||
golang.org/x/sys v0.40.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.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
|
||||
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
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.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
|
||||
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=
|
||||
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
|
||||
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
|
||||
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=
|
||||
|
|
@ -262,6 +287,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.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@ 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
|
||||
}
|
||||
|
||||
|
|
@ -89,7 +88,6 @@ func DefaultConfig() *Config {
|
|||
DNSTimeout: 5 * time.Second,
|
||||
HTTPTimeout: 10 * time.Second,
|
||||
RBLs: []string{},
|
||||
DNSWLs: []string{},
|
||||
CheckAllIPs: false, // By default, only check the first IP
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ func NewEmailAnalyzer(cfg *config.Config) *EmailAnalyzer {
|
|||
cfg.Analysis.DNSTimeout,
|
||||
cfg.Analysis.HTTPTimeout,
|
||||
cfg.Analysis.RBLs,
|
||||
cfg.Analysis.DNSWLs,
|
||||
cfg.Analysis.CheckAllIPs,
|
||||
)
|
||||
|
||||
|
|
@ -121,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)
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,10 +38,9 @@ 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
|
||||
listUnsubscribeURLs []string // URLs from List-Unsubscribe header
|
||||
}
|
||||
|
||||
// NewContentAnalyzer creates a new content analyzer with configurable timeout
|
||||
|
|
@ -116,10 +115,6 @@ func (c *ContentAnalyzer) AnalyzeContent(email *EmailMessage) *ContentResults {
|
|||
// 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()
|
||||
|
|
@ -737,7 +732,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 +878,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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 <id@domain>)")
|
||||
}
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -256,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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,47 +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) (int, string) {
|
||||
// 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, ""
|
||||
}
|
||||
|
||||
scoringListCount := len(r.Lists) - len(r.informationalSet)
|
||||
if scoringListCount <= 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -330,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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -33,10 +33,8 @@ import (
|
|||
type ReportGenerator struct {
|
||||
authAnalyzer *AuthenticationAnalyzer
|
||||
spamAnalyzer *SpamAssassinAnalyzer
|
||||
rspamdAnalyzer *RspamdAnalyzer
|
||||
dnsAnalyzer *DNSAnalyzer
|
||||
rblChecker *DNSListChecker
|
||||
dnswlChecker *DNSListChecker
|
||||
rblChecker *RBLChecker
|
||||
contentAnalyzer *ContentAnalyzer
|
||||
headerAnalyzer *HeaderAnalyzer
|
||||
}
|
||||
|
|
@ -46,16 +44,13 @@ func NewReportGenerator(
|
|||
dnsTimeout time.Duration,
|
||||
httpTimeout time.Duration,
|
||||
rbls []string,
|
||||
dnswls []string,
|
||||
checkAllIPs bool,
|
||||
) *ReportGenerator {
|
||||
return &ReportGenerator{
|
||||
authAnalyzer: NewAuthenticationAnalyzer(),
|
||||
spamAnalyzer: NewSpamAssassinAnalyzer(),
|
||||
rspamdAnalyzer: NewRspamdAnalyzer(),
|
||||
dnsAnalyzer: NewDNSAnalyzer(dnsTimeout),
|
||||
rblChecker: NewRBLChecker(dnsTimeout, rbls, checkAllIPs),
|
||||
dnswlChecker: NewDNSWLChecker(dnsTimeout, dnswls, checkAllIPs),
|
||||
contentAnalyzer: NewContentAnalyzer(httpTimeout),
|
||||
headerAnalyzer: NewHeaderAnalyzer(),
|
||||
}
|
||||
|
|
@ -68,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,9 +78,7 @@ func (r *ReportGenerator) AnalyzeEmail(email *EmailMessage) *AnalysisResults {
|
|||
results.Headers = r.headerAnalyzer.GenerateHeaderAnalysis(email, results.Authentication)
|
||||
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
|
||||
|
|
@ -140,29 +131,13 @@ func (r *ReportGenerator) GenerateReport(testID uuid.UUID, results *AnalysisResu
|
|||
blacklistScore := 0
|
||||
var blacklistGrade string
|
||||
if results.RBL != nil {
|
||||
blacklistScore, blacklistGrade = r.rblChecker.CalculateScore(results.RBL)
|
||||
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{
|
||||
|
|
@ -202,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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -1,155 +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 <contact@happydomain.org>.
|
||||
//
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
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{}
|
||||
|
||||
// NewRspamdAnalyzer creates a new rspamd analyzer
|
||||
func NewRspamdAnalyzer() *RspamdAnalyzer {
|
||||
return &RspamdAnalyzer{}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
result := &api.RspamdResult{
|
||||
Symbols: make(map[string]api.RspamdSymbol),
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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.RspamdSymbol{
|
||||
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)
|
||||
}
|
||||
|
|
@ -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 <contact@happydomain.org>.
|
||||
//
|
||||
// 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 <https://www.gnu.org/licenses/>.
|
||||
|
||||
package analyzer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/mail"
|
||||
"testing"
|
||||
|
||||
"git.happydns.org/happyDeliver/internal/api"
|
||||
)
|
||||
|
||||
func TestAnalyzeRspamdNoHeaders(t *testing.T) {
|
||||
analyzer := NewRspamdAnalyzer()
|
||||
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()
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result := &api.RspamdResult{
|
||||
Symbols: make(map[string]api.RspamdSymbol),
|
||||
}
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
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: <test123@example.com>
|
||||
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()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -69,31 +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 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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ func (a *SpamAssassinAnalyzer) AnalyzeSpamAssassin(email *EmailMessage) *api.Spa
|
|||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
|
|
|
|||
534
web/package-lock.json
generated
534
web/package-lock.json
generated
|
|
@ -519,19 +519,19 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@eslint/compat": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-2.0.2.tgz",
|
||||
"integrity": "sha512-pR1DoD0h3HfF675QZx0xsyrsU8q70Z/plx7880NOhS02NuWLgBCOMDL787nUeQ7EWLkxv3bPQJaarjcPQb2Dwg==",
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/compat/-/compat-2.0.0.tgz",
|
||||
"integrity": "sha512-T9AfE1G1uv4wwq94ozgTGio5EUQBqAVe1X9qsQtSNVEYW6j3hvtZVm8Smr4qL1qDPFg+lOB2cL5RxTRMzq4CTA==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@eslint/core": "^1.1.0"
|
||||
"@eslint/core": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19.0 || ^22.13.0 || >=24"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": "^8.40 || 9 || 10"
|
||||
"eslint": "^8.40 || 9"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"eslint": {
|
||||
|
|
@ -581,9 +581,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@eslint/core": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.1.0.tgz",
|
||||
"integrity": "sha512-/nr9K9wkr3P1EzFTdFdMoLuo1PmIxjmwvPozwoSodjNBdefGujXQUF93u1DDZpEaTuDvMsIQddsd35BwtrW9Xw==",
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.0.0.tgz",
|
||||
"integrity": "sha512-PRfWP+8FOldvbApr6xL7mNCw4cJcSTq4GA7tYbgq15mRb0kWKO/wEB2jr+uwjFH3sZvEZneZyCUGTxsv4Sahyw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
|
|
@ -873,9 +873,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm-eabi": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
|
||||
"integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.54.0.tgz",
|
||||
"integrity": "sha512-OywsdRHrFvCdvsewAInDKCNyR3laPA2mc9bRYJ6LBp5IyvF3fvXbbNR0bSzHlZVFtn6E0xw2oZlyjg4rKCVcng==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
|
|
@ -887,9 +887,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-android-arm64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
|
||||
"integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.54.0.tgz",
|
||||
"integrity": "sha512-Skx39Uv+u7H224Af+bDgNinitlmHyQX1K/atIA32JP3JQw6hVODX5tkbi2zof/E69M1qH2UoN3Xdxgs90mmNYw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -901,9 +901,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-arm64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
|
||||
"integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.54.0.tgz",
|
||||
"integrity": "sha512-k43D4qta/+6Fq+nCDhhv9yP2HdeKeP56QrUUTW7E6PhZP1US6NDqpJj4MY0jBHlJivVJD5P8NxrjuobZBJTCRw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -915,9 +915,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-darwin-x64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
|
||||
"integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.54.0.tgz",
|
||||
"integrity": "sha512-cOo7biqwkpawslEfox5Vs8/qj83M/aZCSSNIWpVzfU2CYHa2G3P1UN5WF01RdTHSgCkri7XOlTdtk17BezlV3A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -929,9 +929,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-arm64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
|
||||
"integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.54.0.tgz",
|
||||
"integrity": "sha512-miSvuFkmvFbgJ1BevMa4CPCFt5MPGw094knM64W9I0giUIMMmRYcGW/JWZDriaw/k1kOBtsWh1z6nIFV1vPNtA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -943,9 +943,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-freebsd-x64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
|
||||
"integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.54.0.tgz",
|
||||
"integrity": "sha512-KGXIs55+b/ZfZsq9aR026tmr/+7tq6VG6MsnrvF4H8VhwflTIuYh+LFUlIsRdQSgrgmtM3fVATzEAj4hBQlaqQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -957,9 +957,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
|
||||
"integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.54.0.tgz",
|
||||
"integrity": "sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
|
|
@ -971,9 +971,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
|
||||
"integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.54.0.tgz",
|
||||
"integrity": "sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
|
|
@ -985,9 +985,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.54.0.tgz",
|
||||
"integrity": "sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -999,9 +999,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-arm64-musl": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
|
||||
"integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.54.0.tgz",
|
||||
"integrity": "sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1013,23 +1013,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-loong64-musl": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
|
||||
"integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.54.0.tgz",
|
||||
"integrity": "sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
|
|
@ -1041,23 +1027,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-ppc64-musl": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
|
||||
"integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.54.0.tgz",
|
||||
"integrity": "sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
|
|
@ -1069,9 +1041,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.54.0.tgz",
|
||||
"integrity": "sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
|
|
@ -1083,9 +1055,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-riscv64-musl": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
|
||||
"integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.54.0.tgz",
|
||||
"integrity": "sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
|
|
@ -1097,9 +1069,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-s390x-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.54.0.tgz",
|
||||
"integrity": "sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
|
|
@ -1111,9 +1083,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.54.0.tgz",
|
||||
"integrity": "sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -1125,9 +1097,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-linux-x64-musl": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
|
||||
"integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.54.0.tgz",
|
||||
"integrity": "sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -1138,24 +1110,10 @@
|
|||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openbsd-x64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
|
||||
"integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-openharmony-arm64": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
|
||||
"integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.54.0.tgz",
|
||||
"integrity": "sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1167,9 +1125,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-arm64-msvc": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
|
||||
"integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.54.0.tgz",
|
||||
"integrity": "sha512-c2V0W1bsKIKfbLMBu/WGBz6Yci8nJ/ZJdheE0EwB73N3MvHYKiKGs3mVilX4Gs70eGeDaMqEob25Tw2Gb9Nqyw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
|
|
@ -1181,9 +1139,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-ia32-msvc": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
|
||||
"integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.54.0.tgz",
|
||||
"integrity": "sha512-woEHgqQqDCkAzrDhvDipnSirm5vxUXtSKDYTVpZG3nUdW/VVB5VdCYA2iReSj/u3yCZzXID4kuKG7OynPnB3WQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
|
|
@ -1195,9 +1153,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-gnu": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
|
||||
"integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.54.0.tgz",
|
||||
"integrity": "sha512-dzAc53LOuFvHwbCEOS0rPbXp6SIhAf2txMP5p6mGyOXXw5mWY8NGGbPMPrs4P1WItkfApDathBj/NzMLUZ9rtQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -1209,9 +1167,9 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
|
||||
"integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.54.0.tgz",
|
||||
"integrity": "sha512-hYT5d3YNdSh3mbCU1gwQyPgQd3T2ne0A3KG8KSBdav5TiBg6eInVmV+TeR5uHufiIgSFg0XsOWGW5/RhNcSvPg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
|
|
@ -1250,9 +1208,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@sveltejs/kit": {
|
||||
"version": "2.50.1",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.50.1.tgz",
|
||||
"integrity": "sha512-XRHD2i3zC4ukhz2iCQzO4mbsts081PAZnnMAQ7LNpWeYgeBmwMsalf0FGSwhFXBbtr2XViPKnFJBDCckWqrsLw==",
|
||||
"version": "2.49.2",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.49.2.tgz",
|
||||
"integrity": "sha512-Vp3zX/qlwerQmHMP6x0Ry1oY7eKKRcOWGc2P59srOp4zcqyn+etJyQpELgOi4+ZSUgteX8Y387NuwruLgGXLUQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
|
|
@ -1262,7 +1220,7 @@
|
|||
"@types/cookie": "^0.6.0",
|
||||
"acorn": "^8.14.1",
|
||||
"cookie": "^0.6.0",
|
||||
"devalue": "^5.6.2",
|
||||
"devalue": "^5.3.2",
|
||||
"esm-env": "^1.2.2",
|
||||
"kleur": "^4.1.5",
|
||||
"magic-string": "^0.30.5",
|
||||
|
|
@ -1281,30 +1239,26 @@
|
|||
"@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",
|
||||
"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"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@opentelemetry/api": {
|
||||
"optional": true
|
||||
},
|
||||
"typescript": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@sveltejs/vite-plugin-svelte": {
|
||||
"version": "6.2.4",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-6.2.4.tgz",
|
||||
"integrity": "sha512-ou/d51QSdTyN26D7h6dSpusAKaZkAiGM55/AKYi+9AGZw7q85hElbjK3kEyzXHhLSnRISHOYzVge6x0jRZ7DXA==",
|
||||
"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",
|
||||
"magic-string": "^0.30.17",
|
||||
"vitefu": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -1316,13 +1270,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@sveltejs/vite-plugin-svelte-inspector": {
|
||||
"version": "5.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte-inspector/-/vite-plugin-svelte-inspector-5.0.2.tgz",
|
||||
"integrity": "sha512-TZzRTcEtZffICSAoZGkPSl6Etsj2torOVrx6Uw0KpXxrec9Gg6jFWQ60Q3+LmNGfZSxHRCZL7vXVZIWmuV50Ig==",
|
||||
"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",
|
||||
"dependencies": {
|
||||
"obug": "^2.1.0"
|
||||
"debug": "^4.4.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.19 || ^22.12 || >=24"
|
||||
|
|
@ -1373,9 +1327,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "24.10.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.9.tgz",
|
||||
"integrity": "sha512-ne4A0IpG3+2ETuREInjPNhUGis1SFjv1d5asp8MzEAGtOZeTeHVDOYqOgqfhvseqg/iXty2hjBf1zAOb7RNiNw==",
|
||||
"version": "24.10.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.4.tgz",
|
||||
"integrity": "sha512-vnDVpYPMzs4wunl27jHrfmwojOGKya0xyM3sH+UE5iv5uPS6vX7UIoh6m+vQc5LGBq52HBKPIn/zcSZVzeDEZg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
|
|
@ -1384,20 +1338,20 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/eslint-plugin": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.54.0.tgz",
|
||||
"integrity": "sha512-hAAP5io/7csFStuOmR782YmTthKBJ9ND3WVL60hcOjvtGFb+HJxH4O5huAcmcZ9v9G8P+JETiZ/G1B8MALnWZQ==",
|
||||
"version": "8.51.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.51.0.tgz",
|
||||
"integrity": "sha512-XtssGWJvypyM2ytBnSnKtHYOGT+4ZwTnBVl36TA4nRO2f4PRNGz5/1OszHzcZCvcBMh+qb7I06uoCmLTRdR9og==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.12.2",
|
||||
"@typescript-eslint/scope-manager": "8.54.0",
|
||||
"@typescript-eslint/type-utils": "8.54.0",
|
||||
"@typescript-eslint/utils": "8.54.0",
|
||||
"@typescript-eslint/visitor-keys": "8.54.0",
|
||||
"ignore": "^7.0.5",
|
||||
"@eslint-community/regexpp": "^4.10.0",
|
||||
"@typescript-eslint/scope-manager": "8.51.0",
|
||||
"@typescript-eslint/type-utils": "8.51.0",
|
||||
"@typescript-eslint/utils": "8.51.0",
|
||||
"@typescript-eslint/visitor-keys": "8.51.0",
|
||||
"ignore": "^7.0.0",
|
||||
"natural-compare": "^1.4.0",
|
||||
"ts-api-utils": "^2.4.0"
|
||||
"ts-api-utils": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
|
@ -1407,7 +1361,7 @@
|
|||
"url": "https://opencollective.com/typescript-eslint"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@typescript-eslint/parser": "^8.54.0",
|
||||
"@typescript-eslint/parser": "^8.51.0",
|
||||
"eslint": "^8.57.0 || ^9.0.0",
|
||||
"typescript": ">=4.8.4 <6.0.0"
|
||||
}
|
||||
|
|
@ -1423,18 +1377,18 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/parser": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.54.0.tgz",
|
||||
"integrity": "sha512-BtE0k6cjwjLZoZixN0t5AKP0kSzlGu7FctRXYuPAm//aaiZhmfq1JwdYpYr1brzEspYyFeF+8XF5j2VK6oalrA==",
|
||||
"version": "8.51.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.51.0.tgz",
|
||||
"integrity": "sha512-3xP4XzzDNQOIqBMWogftkwxhg5oMKApqY0BAflmLZiFYHqyhSOxv/cd/zPQLTcCXr4AkaKb25joocY0BD1WC6A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.54.0",
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
"@typescript-eslint/typescript-estree": "8.54.0",
|
||||
"@typescript-eslint/visitor-keys": "8.54.0",
|
||||
"debug": "^4.4.3"
|
||||
"@typescript-eslint/scope-manager": "8.51.0",
|
||||
"@typescript-eslint/types": "8.51.0",
|
||||
"@typescript-eslint/typescript-estree": "8.51.0",
|
||||
"@typescript-eslint/visitor-keys": "8.51.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
|
@ -1449,15 +1403,15 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/project-service": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.54.0.tgz",
|
||||
"integrity": "sha512-YPf+rvJ1s7MyiWM4uTRhE4DvBXrEV+d8oC3P9Y2eT7S+HBS0clybdMIPnhiATi9vZOYDc7OQ1L/i6ga6NFYK/g==",
|
||||
"version": "8.51.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.51.0.tgz",
|
||||
"integrity": "sha512-Luv/GafO07Z7HpiI7qeEW5NW8HUtZI/fo/kE0YbtQEFpJRUuR0ajcWfCE5bnMvL7QQFrmT/odMe8QZww8X2nfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/tsconfig-utils": "^8.54.0",
|
||||
"@typescript-eslint/types": "^8.54.0",
|
||||
"debug": "^4.4.3"
|
||||
"@typescript-eslint/tsconfig-utils": "^8.51.0",
|
||||
"@typescript-eslint/types": "^8.51.0",
|
||||
"debug": "^4.3.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
|
@ -1471,14 +1425,14 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/scope-manager": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.54.0.tgz",
|
||||
"integrity": "sha512-27rYVQku26j/PbHYcVfRPonmOlVI6gihHtXFbTdB5sb6qA0wdAQAbyXFVarQ5t4HRojIz64IV90YtsjQSSGlQg==",
|
||||
"version": "8.51.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.51.0.tgz",
|
||||
"integrity": "sha512-JhhJDVwsSx4hiOEQPeajGhCWgBMBwVkxC/Pet53EpBVs7zHHtayKefw1jtPaNRXpI9RA2uocdmpdfE7T+NrizA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
"@typescript-eslint/visitor-keys": "8.54.0"
|
||||
"@typescript-eslint/types": "8.51.0",
|
||||
"@typescript-eslint/visitor-keys": "8.51.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
|
@ -1489,9 +1443,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/tsconfig-utils": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.54.0.tgz",
|
||||
"integrity": "sha512-dRgOyT2hPk/JwxNMZDsIXDgyl9axdJI3ogZ2XWhBPsnZUv+hPesa5iuhdYt2gzwA9t8RE5ytOJ6xB0moV0Ujvw==",
|
||||
"version": "8.51.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.51.0.tgz",
|
||||
"integrity": "sha512-Qi5bSy/vuHeWyir2C8u/uqGMIlIDu8fuiYWv48ZGlZ/k+PRPHtaAu7erpc7p5bzw2WNNSniuxoMSO4Ar6V9OXw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
@ -1506,17 +1460,17 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/type-utils": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.54.0.tgz",
|
||||
"integrity": "sha512-hiLguxJWHjjwL6xMBwD903ciAwd7DmK30Y9Axs/etOkftC3ZNN9K44IuRD/EB08amu+Zw6W37x9RecLkOo3pMA==",
|
||||
"version": "8.51.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.51.0.tgz",
|
||||
"integrity": "sha512-0XVtYzxnobc9K0VU7wRWg1yiUrw4oQzexCG2V2IDxxCxhqBMSMbjB+6o91A+Uc0GWtgjCa3Y8bi7hwI0Tu4n5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
"@typescript-eslint/typescript-estree": "8.54.0",
|
||||
"@typescript-eslint/utils": "8.54.0",
|
||||
"debug": "^4.4.3",
|
||||
"ts-api-utils": "^2.4.0"
|
||||
"@typescript-eslint/types": "8.51.0",
|
||||
"@typescript-eslint/typescript-estree": "8.51.0",
|
||||
"@typescript-eslint/utils": "8.51.0",
|
||||
"debug": "^4.3.4",
|
||||
"ts-api-utils": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
|
@ -1531,9 +1485,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/types": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.54.0.tgz",
|
||||
"integrity": "sha512-PDUI9R1BVjqu7AUDsRBbKMtwmjWcn4J3le+5LpcFgWULN3LvHC5rkc9gCVxbrsrGmO1jfPybN5s6h4Jy+OnkAA==",
|
||||
"version": "8.51.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.51.0.tgz",
|
||||
"integrity": "sha512-TizAvWYFM6sSscmEakjY3sPqGwxZRSywSsPEiuZF6d5GmGD9Gvlsv0f6N8FvAAA0CD06l3rIcWNbsN1e5F/9Ag==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
@ -1545,21 +1499,21 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.54.0.tgz",
|
||||
"integrity": "sha512-BUwcskRaPvTk6fzVWgDPdUndLjB87KYDrN5EYGetnktoeAvPtO4ONHlAZDnj5VFnUANg0Sjm7j4usBlnoVMHwA==",
|
||||
"version": "8.51.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.51.0.tgz",
|
||||
"integrity": "sha512-1qNjGqFRmlq0VW5iVlcyHBbCjPB7y6SxpBkrbhNWMy/65ZoncXCEPJxkRZL8McrseNH6lFhaxCIaX+vBuFnRng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/project-service": "8.54.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.54.0",
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
"@typescript-eslint/visitor-keys": "8.54.0",
|
||||
"debug": "^4.4.3",
|
||||
"minimatch": "^9.0.5",
|
||||
"semver": "^7.7.3",
|
||||
"@typescript-eslint/project-service": "8.51.0",
|
||||
"@typescript-eslint/tsconfig-utils": "8.51.0",
|
||||
"@typescript-eslint/types": "8.51.0",
|
||||
"@typescript-eslint/visitor-keys": "8.51.0",
|
||||
"debug": "^4.3.4",
|
||||
"minimatch": "^9.0.4",
|
||||
"semver": "^7.6.0",
|
||||
"tinyglobby": "^0.2.15",
|
||||
"ts-api-utils": "^2.4.0"
|
||||
"ts-api-utils": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
|
@ -1598,30 +1552,17 @@
|
|||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
|
||||
"version": "7.7.3",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
|
||||
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/utils": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.54.0.tgz",
|
||||
"integrity": "sha512-9Cnda8GS57AQakvRyG0PTejJNlA2xhvyNtEVIMlDWOOeEyBkYWhGPnfrIAnqxLMTSTo6q8g12XVjjev5l1NvMA==",
|
||||
"version": "8.51.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.51.0.tgz",
|
||||
"integrity": "sha512-11rZYxSe0zabiKaCP2QAwRf/dnmgFgvTmeDTtZvUvXG3UuAdg/GU02NExmmIXzz3vLGgMdtrIosI84jITQOxUA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.9.1",
|
||||
"@typescript-eslint/scope-manager": "8.54.0",
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
"@typescript-eslint/typescript-estree": "8.54.0"
|
||||
"@eslint-community/eslint-utils": "^4.7.0",
|
||||
"@typescript-eslint/scope-manager": "8.51.0",
|
||||
"@typescript-eslint/types": "8.51.0",
|
||||
"@typescript-eslint/typescript-estree": "8.51.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
|
@ -1636,13 +1577,13 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/visitor-keys": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.54.0.tgz",
|
||||
"integrity": "sha512-VFlhGSl4opC0bprJiItPQ1RfUhGDIBokcPwaFH4yiBCaNPeld/9VeXbiPO1cLyorQi1G1vL+ecBk1x8o1axORA==",
|
||||
"version": "8.51.0",
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.51.0.tgz",
|
||||
"integrity": "sha512-mM/JRQOzhVN1ykejrvwnBRV3+7yTKK8tVANVN3o1O0t0v7o+jqdVu9crPy5Y9dov15TJk/FTIgoUGHrTOVL3Zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/types": "8.54.0",
|
||||
"@typescript-eslint/types": "8.51.0",
|
||||
"eslint-visitor-keys": "^4.2.1"
|
||||
},
|
||||
"engines": {
|
||||
|
|
@ -2275,9 +2216,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/devalue": {
|
||||
"version": "5.6.2",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.2.tgz",
|
||||
"integrity": "sha512-nPRkjWzzDQlsejL1WVifk5rvcFi/y1onBRxjaFMjZeR9mFpqu2gmAZ9xUB9/IEanEP/vBtGeGganC/GO1fmufg==",
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.1.tgz",
|
||||
"integrity": "sha512-jDwizj+IlEZBunHcOuuFVBnIMPAEHvTsJj0BcIp94xYguLRVBcXO853px/MyIJvbVzWdsGvrRweIUWJw8hBP7A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
|
|
@ -2434,9 +2375,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-svelte": {
|
||||
"version": "3.14.0",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.14.0.tgz",
|
||||
"integrity": "sha512-Isw0GvaMm0yHxAj71edAdGFh28ufYs+6rk2KlbbZphnqZAzrH3Se3t12IFh2H9+1F/jlDhBBL4oiOJmLqmYX0g==",
|
||||
"version": "3.13.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-3.13.1.tgz",
|
||||
"integrity": "sha512-Ng+kV/qGS8P/isbNYVE3sJORtubB+yLEcYICMkUWNaDTb0SwZni/JhAYXh/Dz/q2eThUwWY0VMPZ//KYD1n3eQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -2562,9 +2503,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/esrap": {
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.2.tgz",
|
||||
"integrity": "sha512-zA6497ha+qKvoWIK+WM9NAh5ni17sKZKhbS5B3PoYbBvaYHZWoS33zmFybmyqpn07RLUxSmn+RCls2/XF+d0oQ==",
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/esrap/-/esrap-2.2.1.tgz",
|
||||
"integrity": "sha512-GiYWG34AN/4CUyaWAgunGt0Rxvr1PTMlGC0vvEov/uOQYWne2bpN03Um+k8jT+q3op33mKouP2zeJ6OlM+qeUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -2768,9 +2709,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/globals": {
|
||||
"version": "17.3.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-17.3.0.tgz",
|
||||
"integrity": "sha512-yMqGUQVVCkD4tqjOJf3TnrvaaHDMYp4VlUSObbkIiuCPe/ofdMBFIAcBbCSRFWOnos6qRiTVStDwqPLUclaxIw==",
|
||||
"version": "17.0.0",
|
||||
"resolved": "https://registry.npmjs.org/globals/-/globals-17.0.0.tgz",
|
||||
"integrity": "sha512-gv5BeD2EssA793rlFWVPMMCqefTlpusw6/2TbAVMy0FzcG8wKJn4O+NqJ4+XWmmwrayJgw5TzrmWjFgmz1XPqw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
|
@ -3066,9 +3007,9 @@
|
|||
}
|
||||
},
|
||||
"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"
|
||||
},
|
||||
|
|
@ -3187,41 +3128,25 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nypm": {
|
||||
"version": "0.6.4",
|
||||
"resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.4.tgz",
|
||||
"integrity": "sha512-1TvCKjZyyklN+JJj2TS3P4uSQEInrM/HkkuSXsEzm1ApPgBffOn8gFguNnZf07r/1X6vlryfIqMUkJKQMzlZiw==",
|
||||
"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.0",
|
||||
"resolved": "https://registry.npmjs.org/citty/-/citty-0.2.0.tgz",
|
||||
"integrity": "sha512-8csy5IBFI2ex2hTVpaHN2j+LNE199AgiI7y4dMintrr8i0lQiFn+0AWMZrWdHKIgMOer65f8IThysYhoReqjWA==",
|
||||
"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",
|
||||
|
|
@ -3349,9 +3274,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"
|
||||
},
|
||||
|
|
@ -3537,9 +3462,9 @@
|
|||
}
|
||||
},
|
||||
"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.7.4",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz",
|
||||
"integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
|
|
@ -3610,9 +3535,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "4.57.1",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.57.1.tgz",
|
||||
"integrity": "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A==",
|
||||
"version": "4.54.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.54.0.tgz",
|
||||
"integrity": "sha512-3nk8Y3a9Ea8szgKhinMlGMhGMw89mqule3KWczxhIzqudyHdCIOHw8WJlj/r329fACjKLEh13ZSk7oE22kyeIw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -3626,31 +3551,28 @@
|
|||
"npm": ">=8.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@rollup/rollup-android-arm-eabi": "4.57.1",
|
||||
"@rollup/rollup-android-arm64": "4.57.1",
|
||||
"@rollup/rollup-darwin-arm64": "4.57.1",
|
||||
"@rollup/rollup-darwin-x64": "4.57.1",
|
||||
"@rollup/rollup-freebsd-arm64": "4.57.1",
|
||||
"@rollup/rollup-freebsd-x64": "4.57.1",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.57.1",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.57.1",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-loong64-musl": "4.57.1",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-ppc64-musl": "4.57.1",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.57.1",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.57.1",
|
||||
"@rollup/rollup-linux-x64-musl": "4.57.1",
|
||||
"@rollup/rollup-openbsd-x64": "4.57.1",
|
||||
"@rollup/rollup-openharmony-arm64": "4.57.1",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.57.1",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.57.1",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.57.1",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.57.1",
|
||||
"@rollup/rollup-android-arm-eabi": "4.54.0",
|
||||
"@rollup/rollup-android-arm64": "4.54.0",
|
||||
"@rollup/rollup-darwin-arm64": "4.54.0",
|
||||
"@rollup/rollup-darwin-x64": "4.54.0",
|
||||
"@rollup/rollup-freebsd-arm64": "4.54.0",
|
||||
"@rollup/rollup-freebsd-x64": "4.54.0",
|
||||
"@rollup/rollup-linux-arm-gnueabihf": "4.54.0",
|
||||
"@rollup/rollup-linux-arm-musleabihf": "4.54.0",
|
||||
"@rollup/rollup-linux-arm64-gnu": "4.54.0",
|
||||
"@rollup/rollup-linux-arm64-musl": "4.54.0",
|
||||
"@rollup/rollup-linux-loong64-gnu": "4.54.0",
|
||||
"@rollup/rollup-linux-ppc64-gnu": "4.54.0",
|
||||
"@rollup/rollup-linux-riscv64-gnu": "4.54.0",
|
||||
"@rollup/rollup-linux-riscv64-musl": "4.54.0",
|
||||
"@rollup/rollup-linux-s390x-gnu": "4.54.0",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.54.0",
|
||||
"@rollup/rollup-linux-x64-musl": "4.54.0",
|
||||
"@rollup/rollup-openharmony-arm64": "4.54.0",
|
||||
"@rollup/rollup-win32-arm64-msvc": "4.54.0",
|
||||
"@rollup/rollup-win32-ia32-msvc": "4.54.0",
|
||||
"@rollup/rollup-win32-x64-gnu": "4.54.0",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.54.0",
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
|
|
@ -3819,9 +3741,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/svelte": {
|
||||
"version": "5.49.1",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.49.1.tgz",
|
||||
"integrity": "sha512-jj95WnbKbXsXXngYj28a4zx8jeZx50CN/J4r0CEeax2pbfdsETv/J1K8V9Hbu3DCXnpHz5qAikICuxEooi7eNQ==",
|
||||
"version": "5.46.1",
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.46.1.tgz",
|
||||
"integrity": "sha512-ynjfCHD3nP2el70kN5Pmg37sSi0EjOm9FgHYQdC4giWG/hzO3AatzXXJJgP305uIhGQxSufJLuYWtkY8uK/8RA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
|
|
@ -3834,9 +3756,9 @@
|
|||
"aria-query": "^5.3.1",
|
||||
"axobject-query": "^4.1.0",
|
||||
"clsx": "^2.1.1",
|
||||
"devalue": "^5.6.2",
|
||||
"devalue": "^5.5.0",
|
||||
"esm-env": "^1.2.1",
|
||||
"esrap": "^2.2.2",
|
||||
"esrap": "^2.2.1",
|
||||
"is-reference": "^3.0.3",
|
||||
"locate-character": "^3.0.0",
|
||||
"magic-string": "^0.30.11",
|
||||
|
|
@ -3847,9 +3769,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/svelte-check": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.6.tgz",
|
||||
"integrity": "sha512-uBkz96ElE3G4pt9E1Tw0xvBfIUQkeH794kDQZdAUk795UVMr+NJZpuFSS62vcmO/DuSalK83LyOwhgWq8YGU1Q==",
|
||||
"version": "4.3.5",
|
||||
"resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-4.3.5.tgz",
|
||||
"integrity": "sha512-e4VWZETyXaKGhpkxOXP+B/d0Fp/zKViZoJmneZWe/05Y2aqSKj3YN2nLfYPJBQ87WEiY4BQCQ9hWGu9mPT1a1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
|
@ -4016,16 +3938,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/typescript-eslint": {
|
||||
"version": "8.54.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.54.0.tgz",
|
||||
"integrity": "sha512-CKsJ+g53QpsNPqbzUsfKVgd3Lny4yKZ1pP4qN3jdMOg/sisIDLGyDMezycquXLE5JsEU0wp3dGNdzig0/fmSVQ==",
|
||||
"version": "8.51.0",
|
||||
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.51.0.tgz",
|
||||
"integrity": "sha512-jh8ZuM5oEh2PSdyQG9YAEM1TCGuWenLSuSUhf/irbVUNW9O5FhbFVONviN2TgMTBnUmyHv7E56rYnfLZK6TkiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "8.54.0",
|
||||
"@typescript-eslint/parser": "8.54.0",
|
||||
"@typescript-eslint/typescript-estree": "8.54.0",
|
||||
"@typescript-eslint/utils": "8.54.0"
|
||||
"@typescript-eslint/eslint-plugin": "8.51.0",
|
||||
"@typescript-eslint/parser": "8.51.0",
|
||||
"@typescript-eslint/typescript-estree": "8.51.0",
|
||||
"@typescript-eslint/utils": "8.51.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
|
|
@ -4078,9 +4000,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz",
|
||||
"integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==",
|
||||
"version": "7.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-7.3.0.tgz",
|
||||
"integrity": "sha512-dZwN5L1VlUBewiP6H9s2+B3e3Jg96D0vzN+Ry73sOefebhYr9f94wwkMNN/9ouoU8pV1BqA1d1zGk8928cx0rg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@
|
|||
case "domain_pass":
|
||||
case "orgdomain_pass":
|
||||
return "text-success";
|
||||
case "permerror":
|
||||
case "error":
|
||||
case "fail":
|
||||
case "missing":
|
||||
|
|
@ -52,7 +51,6 @@
|
|||
case "neutral":
|
||||
case "invalid":
|
||||
case "null":
|
||||
case "permerror":
|
||||
case "error":
|
||||
case "null_smtp":
|
||||
case "null_header":
|
||||
|
|
|
|||
|
|
@ -1,21 +1,23 @@
|
|||
<script lang="ts">
|
||||
import type { BlacklistCheck } from "$lib/api/types.gen";
|
||||
import type { BlacklistCheck, ReceivedHop } from "$lib/api/types.gen";
|
||||
import { getScoreColorClass } from "$lib/score";
|
||||
import { theme } from "$lib/stores/theme";
|
||||
import EmailPathCard from "./EmailPathCard.svelte";
|
||||
import GradeDisplay from "./GradeDisplay.svelte";
|
||||
|
||||
interface Props {
|
||||
blacklists: Record<string, BlacklistCheck[]>;
|
||||
blacklistGrade?: string;
|
||||
blacklistScore?: number;
|
||||
receivedChain?: ReceivedHop[];
|
||||
}
|
||||
|
||||
let { blacklists, blacklistGrade, blacklistScore }: Props = $props();
|
||||
let { blacklists, blacklistGrade, blacklistScore, receivedChain }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="card shadow-sm" id="rbl-details">
|
||||
<div class="card-header" class:bg-white={$theme === "light"} class:bg-dark={$theme !== "light"}>
|
||||
<h4 class="mb-0 d-flex flex-wrap justify-content-between align-items-center">
|
||||
<h4 class="mb-0 d-flex justify-content-between align-items-center">
|
||||
<span>
|
||||
<i class="bi bi-shield-exclamation me-2"></i>
|
||||
Blacklist Checks
|
||||
|
|
@ -33,7 +35,11 @@
|
|||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row row-cols-1 row-cols-lg-2 overflow-auto">
|
||||
{#if receivedChain}
|
||||
<EmailPathCard {receivedChain} />
|
||||
{/if}
|
||||
|
||||
<div class="row row-cols-1 row-cols-lg-2">
|
||||
{#each Object.entries(blacklists) as [ip, checks]}
|
||||
<div class="col mb-3">
|
||||
<h5 class="text-muted">
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
<script lang="ts">
|
||||
import type { ReceivedHop } from "$lib/api/types.gen";
|
||||
import { theme } from "$lib/stores/theme";
|
||||
|
||||
interface Props {
|
||||
receivedChain: ReceivedHop[];
|
||||
|
|
@ -10,18 +9,9 @@
|
|||
</script>
|
||||
|
||||
{#if receivedChain && receivedChain.length > 0}
|
||||
<div class="card shadow-sm" id="email-path">
|
||||
<div
|
||||
class="card-header"
|
||||
class:bg-white={$theme === "light"}
|
||||
class:bg-dark={$theme !== "light"}
|
||||
>
|
||||
<h4 class="mb-0">
|
||||
<i class="bi bi-pin-map me-2"></i>
|
||||
Email Path
|
||||
</h4>
|
||||
</div>
|
||||
<div class="list-group list-group-flush">
|
||||
<div class="mb-3" id="email-path">
|
||||
<h5>Email Path (Received Chain)</h5>
|
||||
<div class="list-group">
|
||||
{#each receivedChain as hop, i}
|
||||
<div class="list-group-item">
|
||||
<div class="d-flex w-100 justify-content-between">
|
||||
|
|
@ -40,7 +30,7 @@
|
|||
: "-"}
|
||||
</small>
|
||||
</div>
|
||||
{#if hop.with || hop.id || hop.from}
|
||||
{#if hop.with || hop.id}
|
||||
<p class="mb-1 small d-flex gap-3">
|
||||
{#if hop.with}
|
||||
<span>
|
||||
|
|
|
|||
|
|
@ -21,11 +21,6 @@
|
|||
);
|
||||
|
||||
const hasForwardRecords = $derived(ptrForwardRecords && ptrForwardRecords.length > 0);
|
||||
|
||||
let showDifferent = $state(false);
|
||||
const differentCount = $derived(
|
||||
ptrForwardRecords ? ptrForwardRecords.filter((ip) => ip !== senderIp).length : 0,
|
||||
);
|
||||
</script>
|
||||
|
||||
{#if ptrRecords && ptrRecords.length > 0}
|
||||
|
|
@ -68,31 +63,15 @@
|
|||
<div class="mb-2">
|
||||
<strong>Forward Resolution (A/AAAA):</strong>
|
||||
{#each ptrForwardRecords as ip}
|
||||
{#if ip === senderIp || !fcrDnsIsValid || showDifferent}
|
||||
<div class="d-flex gap-2 align-items-center mt-1">
|
||||
{#if senderIp && ip === senderIp}
|
||||
<span class="badge bg-success">Match</span>
|
||||
{:else}
|
||||
<span class="badge bg-secondary">Different</span>
|
||||
{/if}
|
||||
<code>{ip}</code>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
{#if fcrDnsIsValid && differentCount > 0}
|
||||
<div class="mt-1">
|
||||
<button
|
||||
class="btn btn-link btn-sm p-0 text-muted"
|
||||
onclick={() => (showDifferent = !showDifferent)}
|
||||
>
|
||||
{#if showDifferent}
|
||||
Hide other IPs
|
||||
{:else}
|
||||
Show {differentCount} other IP{differentCount > 1 ? 's' : ''} (not the sender)
|
||||
{/if}
|
||||
</button>
|
||||
<div class="d-flex gap-2 align-items-center mt-1">
|
||||
{#if senderIp && ip === senderIp}
|
||||
<span class="badge bg-success">Match</span>
|
||||
{:else}
|
||||
<span class="badge bg-warning">Different</span>
|
||||
{/if}
|
||||
<code>{ip}</code>
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{#if fcrDnsIsValid}
|
||||
<div class="alert alert-success mb-0 mt-2">
|
||||
|
|
|
|||
|
|
@ -1,146 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { RspamdResult } from "$lib/api/types.gen";
|
||||
import { getScoreColorClass } from "$lib/score";
|
||||
import { theme } from "$lib/stores/theme";
|
||||
import GradeDisplay from "./GradeDisplay.svelte";
|
||||
|
||||
interface Props {
|
||||
rspamd: RspamdResult;
|
||||
}
|
||||
|
||||
let { rspamd }: Props = $props();
|
||||
|
||||
// Derive effective action from score vs known rspamd default thresholds.
|
||||
// The action header is unreliable in milter setups (always "no action").
|
||||
const RSPAMD_GREYLIST_THRESHOLD = 4;
|
||||
const RSPAMD_ADD_HEADER_THRESHOLD = 6;
|
||||
|
||||
const effectiveAction = $derived.by(() => {
|
||||
const rejectThreshold = rspamd.threshold > 0 ? rspamd.threshold : 15;
|
||||
if (rspamd.score >= rejectThreshold) return { label: "Reject", cls: "bg-danger" };
|
||||
if (rspamd.score >= RSPAMD_ADD_HEADER_THRESHOLD)
|
||||
return { label: "Add header", cls: "bg-warning text-dark" };
|
||||
if (rspamd.score >= RSPAMD_GREYLIST_THRESHOLD)
|
||||
return { label: "Greylist", cls: "bg-warning text-dark" };
|
||||
return { label: "No action", cls: "bg-success" };
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="card shadow-sm" id="rspamd-details">
|
||||
<div class="card-header {$theme === 'light' ? 'bg-white' : 'bg-dark'}">
|
||||
<h4 class="mb-0 d-flex justify-content-between align-items-center">
|
||||
<span>
|
||||
<i class="bi bi-bug me-2"></i>
|
||||
rspamd Analysis
|
||||
</span>
|
||||
<span>
|
||||
{#if rspamd.deliverability_score !== undefined}
|
||||
<span class="badge bg-{getScoreColorClass(rspamd.deliverability_score)}">
|
||||
{rspamd.deliverability_score}%
|
||||
</span>
|
||||
{/if}
|
||||
{#if rspamd.deliverability_grade !== undefined}
|
||||
<GradeDisplay grade={rspamd.deliverability_grade} size="small" />
|
||||
{/if}
|
||||
</span>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="row mb-3">
|
||||
<div class="col-md-4">
|
||||
<strong>Score:</strong>
|
||||
<span class={rspamd.is_spam ? "text-danger" : "text-success"}>
|
||||
{rspamd.score.toFixed(2)} / {rspamd.threshold.toFixed(1)}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<strong>Classified as:</strong>
|
||||
<span class="badge {rspamd.is_spam ? 'bg-danger' : 'bg-success'} ms-2">
|
||||
{rspamd.is_spam ? "SPAM" : "HAM"}
|
||||
</span>
|
||||
</div>
|
||||
<div class="col-md-4">
|
||||
<strong>Action:</strong>
|
||||
<span class="badge {effectiveAction.cls} ms-2">
|
||||
{effectiveAction.label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if rspamd.symbols && Object.keys(rspamd.symbols).length > 0}
|
||||
<div class="mb-3">
|
||||
<div class="table-responsive mt-2">
|
||||
<table class="table table-sm table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Symbol</th>
|
||||
<th class="text-end">Score</th>
|
||||
<th>Parameters</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each Object.entries(rspamd.symbols).sort(([, a], [, b]) => b.score - a.score) as [symbolName, symbol]}
|
||||
<tr
|
||||
class={symbol.score > 0
|
||||
? "table-warning"
|
||||
: symbol.score < 0
|
||||
? "table-success"
|
||||
: ""}
|
||||
>
|
||||
<td class="font-monospace">{symbolName}</td>
|
||||
<td class="text-end">
|
||||
<span
|
||||
class={symbol.score > 0
|
||||
? "text-danger fw-bold"
|
||||
: symbol.score < 0
|
||||
? "text-success fw-bold"
|
||||
: "text-muted"}
|
||||
>
|
||||
{symbol.score > 0 ? "+" : ""}{symbol.score.toFixed(2)}
|
||||
</span>
|
||||
</td>
|
||||
<td class="small text-muted">{symbol.params ?? ""}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if rspamd.report}
|
||||
<details class="mt-3">
|
||||
<summary class="cursor-pointer fw-bold">Raw Report</summary>
|
||||
<pre
|
||||
class="mt-2 small {$theme === 'light'
|
||||
? 'bg-light'
|
||||
: 'bg-secondary'} p-3 rounded">{rspamd.report}</pre>
|
||||
</details>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.cursor-pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
details summary {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
details summary:hover {
|
||||
color: var(--bs-primary);
|
||||
}
|
||||
|
||||
/* Darker table colors in dark mode */
|
||||
:global([data-bs-theme="dark"]) .table-warning {
|
||||
--bs-table-bg: rgba(255, 193, 7, 0.2);
|
||||
--bs-table-border-color: rgba(255, 193, 7, 0.3);
|
||||
}
|
||||
|
||||
:global([data-bs-theme="dark"]) .table-success {
|
||||
--bs-table-bg: rgba(25, 135, 84, 0.2);
|
||||
--bs-table-border-color: rgba(25, 135, 84, 0.3);
|
||||
}
|
||||
</style>
|
||||
|
|
@ -6,9 +6,11 @@
|
|||
|
||||
interface Props {
|
||||
spamassassin: SpamAssassinResult;
|
||||
spamGrade?: string;
|
||||
spamScore?: number;
|
||||
}
|
||||
|
||||
let { spamassassin }: Props = $props();
|
||||
let { spamassassin, spamGrade, spamScore }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="card shadow-sm" id="spam-details">
|
||||
|
|
@ -19,13 +21,13 @@
|
|||
SpamAssassin Analysis
|
||||
</span>
|
||||
<span>
|
||||
{#if spamassassin.deliverability_score !== undefined}
|
||||
<span class="badge bg-{getScoreColorClass(spamassassin.deliverability_score)}">
|
||||
{spamassassin.deliverability_score}%
|
||||
{#if spamScore !== undefined}
|
||||
<span class="badge bg-{getScoreColorClass(spamScore)}">
|
||||
{spamScore}%
|
||||
</span>
|
||||
{/if}
|
||||
{#if spamassassin.deliverability_grade !== undefined}
|
||||
<GradeDisplay grade={spamassassin.deliverability_grade} size="small" />
|
||||
{#if spamGrade !== undefined}
|
||||
<GradeDisplay grade={spamGrade} size="small" />
|
||||
{/if}
|
||||
</span>
|
||||
</h4>
|
||||
|
|
|
|||
|
|
@ -113,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" });
|
||||
|
|
@ -331,7 +331,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 " });
|
||||
|
|
@ -422,17 +422,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;
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
<script lang="ts">
|
||||
import type { BlacklistCheck } from "$lib/api/types.gen";
|
||||
import { theme } from "$lib/stores/theme";
|
||||
|
||||
interface Props {
|
||||
whitelists: Record<string, BlacklistCheck[]>;
|
||||
}
|
||||
|
||||
let { whitelists }: Props = $props();
|
||||
</script>
|
||||
|
||||
<div class="card shadow-sm" id="dnswl-details">
|
||||
<div class="card-header" class:bg-white={$theme === "light"} class:bg-dark={$theme !== "light"}>
|
||||
<h4 class="mb-0 d-flex flex-wrap justify-content-between align-items-center">
|
||||
<span>
|
||||
<i class="bi bi-shield-check me-2"></i>
|
||||
Whitelist Checks
|
||||
</span>
|
||||
<span class="badge bg-info text-white">Informational</span>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="text-muted small mb-3">
|
||||
DNS whitelists identify trusted senders. Being listed here is a positive signal, but has
|
||||
no impact on the overall score.
|
||||
</p>
|
||||
|
||||
<div class="row row-cols-1 row-cols-lg-2 overflow-auto">
|
||||
{#each Object.entries(whitelists) as [ip, checks]}
|
||||
<div class="col mb-3">
|
||||
<h5 class="text-muted">
|
||||
<i class="bi bi-hdd-network me-1"></i>
|
||||
{ip}
|
||||
</h5>
|
||||
<table class="table table-sm table-striped table-hover mb-0">
|
||||
<tbody>
|
||||
{#each checks as check}
|
||||
<tr>
|
||||
<td title={check.response || "-"}>
|
||||
<span
|
||||
class="badge"
|
||||
class:bg-success={check.listed}
|
||||
class:bg-dark={check.error}
|
||||
class:bg-secondary={!check.listed && !check.error}
|
||||
>
|
||||
{check.error
|
||||
? "Error"
|
||||
: check.listed
|
||||
? "Listed"
|
||||
: "Not listed"}
|
||||
</span>
|
||||
</td>
|
||||
<td><code>{check.rbl}</code></td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -19,9 +19,7 @@ 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 TinySurvey } from "./TinySurvey.svelte";
|
||||
export { default as WhitelistCard } from "./WhitelistCard.svelte";
|
||||
|
|
|
|||
|
|
@ -25,7 +25,6 @@ interface AppConfig {
|
|||
report_retention?: number;
|
||||
survey_url?: string;
|
||||
custom_logo_url?: string;
|
||||
rbls?: string[];
|
||||
}
|
||||
|
||||
const defaultConfig: AppConfig = {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ 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";
|
||||
};
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
import { onMount } from "svelte";
|
||||
import { checkBlacklist } from "$lib/api";
|
||||
import type { BlacklistCheckResponse } from "$lib/api/types.gen";
|
||||
import { BlacklistCard, GradeDisplay, TinySurvey, WhitelistCard } from "$lib/components";
|
||||
import { BlacklistCard, GradeDisplay, TinySurvey } from "$lib/components";
|
||||
import { theme } from "$lib/stores/theme";
|
||||
|
||||
let ip = $derived($page.params.ip);
|
||||
|
|
@ -28,7 +28,7 @@
|
|||
});
|
||||
|
||||
if (response.response.ok) {
|
||||
result = response.data ?? null;
|
||||
result = response.data;
|
||||
} else if (response.error) {
|
||||
error = response.error.message || "Failed to check IP address";
|
||||
}
|
||||
|
|
@ -122,8 +122,8 @@
|
|||
>
|
||||
<p class="mb-0 mt-1 small">
|
||||
This IP address is listed on {result.listed_count} of
|
||||
{result.blacklists.length} checked blacklist{result
|
||||
.blacklists.length > 1
|
||||
{result.checks.length} checked blacklist{result
|
||||
.checks.length > 1
|
||||
? "s"
|
||||
: ""}.
|
||||
</p>
|
||||
|
|
@ -150,23 +150,12 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<!-- Blacklist Results Card -->
|
||||
<div class="col col-lg-6">
|
||||
<BlacklistCard
|
||||
blacklists={{ [result.ip]: result.blacklists }}
|
||||
blacklistScore={result.score}
|
||||
blacklistGrade={result.grade}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Whitelist Results Card -->
|
||||
{#if result.whitelists && result.whitelists.length > 0}
|
||||
<div class="col col-lg-6">
|
||||
<WhitelistCard whitelists={{ [result.ip]: result.whitelists }} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
<!-- Blacklist Results Card -->
|
||||
<BlacklistCard
|
||||
blacklists={{ [result.ip]: result.checks }}
|
||||
blacklistScore={result.score}
|
||||
blacklistGrade={result.grade}
|
||||
/>
|
||||
|
||||
<!-- Information Card -->
|
||||
<div class="card shadow-sm mt-4">
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@
|
|||
<div class="d-flex justify-content-end me-lg-5 mt-3">
|
||||
<TinySurvey
|
||||
class="bg-primary-subtle rounded-4 p-3 text-center"
|
||||
source={"domain-" + result.domain}
|
||||
source={"rbl-" + result.ip}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,26 +3,21 @@
|
|||
import { onDestroy } from "svelte";
|
||||
|
||||
import { getReport, getTest, reanalyzeReport } from "$lib/api";
|
||||
import type { BlacklistCheck, Report, Test } from "$lib/api/types.gen";
|
||||
import type { Report, Test } from "$lib/api/types.gen";
|
||||
import {
|
||||
AuthenticationCard,
|
||||
BlacklistCard,
|
||||
ContentAnalysisCard,
|
||||
DnsRecordsCard,
|
||||
EmailPathCard,
|
||||
ErrorDisplay,
|
||||
HeaderAnalysisCard,
|
||||
PendingState,
|
||||
RspamdCard,
|
||||
ScoreCard,
|
||||
SpamAssassinCard,
|
||||
SummaryCard,
|
||||
TinySurvey,
|
||||
WhitelistCard,
|
||||
} from "$lib/components";
|
||||
|
||||
type BlacklistRecords = Record<string, BlacklistCheck[]>;
|
||||
|
||||
let testId = $derived(page.params.test);
|
||||
let test = $state<Test | null>(null);
|
||||
let report = $state<Report | null>(null);
|
||||
|
|
@ -295,15 +290,6 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Received Chain -->
|
||||
{#if report.header_analysis?.received_chain && report.header_analysis.received_chain.length > 0}
|
||||
<div class="row mb-4" id="received-chain">
|
||||
<div class="col-12">
|
||||
<EmailPathCard receivedChain={report.header_analysis.received_chain} />
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- DNS Records -->
|
||||
{#if report.dns_results}
|
||||
<div class="row mb-4" id="dns">
|
||||
|
|
@ -334,45 +320,17 @@
|
|||
{/if}
|
||||
|
||||
<!-- Blacklist Checks -->
|
||||
{#snippet blacklistChecks(blacklists: BlacklistRecords, report: Report)}
|
||||
<BlacklistCard
|
||||
{blacklists}
|
||||
blacklistGrade={report.summary?.blacklist_grade}
|
||||
blacklistScore={report.summary?.blacklist_score}
|
||||
/>
|
||||
{/snippet}
|
||||
|
||||
<!-- Whitelist Checks -->
|
||||
{#snippet whitelistChecks(whitelists: BlacklistRecords)}
|
||||
<WhitelistCard {whitelists} />
|
||||
{/snippet}
|
||||
|
||||
<!-- Blacklist & Whitelist Checks -->
|
||||
{#if report.blacklists && report.whitelists && Object.keys(report.blacklists).length == 1 && Object.keys(report.whitelists).length == 1}
|
||||
<div class="row mb-4">
|
||||
<div class="col-6" id="blacklist">
|
||||
{@render blacklistChecks(report.blacklists, report)}
|
||||
</div>
|
||||
<div class="col-6" id="whitelist">
|
||||
{@render whitelistChecks(report.whitelists)}
|
||||
{#if report.blacklists && Object.keys(report.blacklists).length > 0}
|
||||
<div class="row mb-4" id="blacklist">
|
||||
<div class="col-12">
|
||||
<BlacklistCard
|
||||
blacklists={report.blacklists}
|
||||
blacklistGrade={report.summary?.blacklist_grade}
|
||||
blacklistScore={report.summary?.blacklist_score}
|
||||
receivedChain={report.header_analysis?.received_chain}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
{#if report.blacklists && Object.keys(report.blacklists).length > 0}
|
||||
<div class="row mb-4" id="blacklist">
|
||||
<div class="col-12">
|
||||
{@render blacklistChecks(report.blacklists, report)}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if report.whitelists && Object.keys(report.whitelists).length > 0}
|
||||
<div class="row mb-4" id="whitelist">
|
||||
<div class="col-12">
|
||||
{@render whitelistChecks(report.whitelists)}
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<!-- Header Analysis -->
|
||||
|
|
@ -389,19 +347,16 @@
|
|||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Spam filter analysis -->
|
||||
{#if report.spamassassin || report.rspamd}
|
||||
<!-- Additional Information -->
|
||||
{#if report.spamassassin}
|
||||
<div class="row mb-4" id="spam">
|
||||
{#if report.spamassassin}
|
||||
<div class={report.rspamd ? "col col-lg-6 mb-4 mb-lg-0" : "col-12"}>
|
||||
<SpamAssassinCard spamassassin={report.spamassassin} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if report.rspamd}
|
||||
<div class={report.spamassassin ? "col col-lg-6" : "col-12"}>
|
||||
<RspamdCard rspamd={report.rspamd} />
|
||||
</div>
|
||||
{/if}
|
||||
<div class="col-12">
|
||||
<SpamAssassinCard
|
||||
spamassassin={report.spamassassin}
|
||||
spamGrade={report.summary?.spam_grade}
|
||||
spamScore={report.summary?.spam_score}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue