From 6f22d340d2907bc480e3ba29f9acd89fbf58c826 Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Fri, 31 Oct 2025 11:01:58 +0700 Subject: [PATCH] New route to check blacklist only --- api/openapi.yaml | 78 +++++++ internal/api/handlers.go | 39 ++++ pkg/analyzer/analyzer.go | 20 ++ pkg/analyzer/rbl.go | 22 ++ web/routes.go | 4 + web/src/lib/stores/config.ts | 1 + web/src/routes/+page.svelte | 8 +- web/src/routes/blacklist/+page.svelte | 186 +++++++++++++++++ web/src/routes/blacklist/[ip]/+page.svelte | 230 +++++++++++++++++++++ 9 files changed, 586 insertions(+), 2 deletions(-) create mode 100644 web/src/routes/blacklist/+page.svelte create mode 100644 web/src/routes/blacklist/[ip]/+page.svelte diff --git a/api/openapi.yaml b/api/openapi.yaml index 8c8a836..92bf3e3 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -202,6 +202,39 @@ paths: schema: $ref: '#/components/schemas/Error' + /blacklist: + post: + tags: + - tests + summary: Check an IP address against DNS blacklists + description: Tests a single IP address (IPv4 or IPv6) against configured DNS-based blacklists (RBLs) without requiring an actual email to be sent. Returns results immediately. + operationId: checkBlacklist + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BlacklistCheckRequest' + responses: + '200': + description: Blacklist check completed successfully + content: + application/json: + schema: + $ref: '#/components/schemas/BlacklistCheckResponse' + '400': + description: Invalid request + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + '500': + description: Internal server error + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /status: get: tags: @@ -1182,3 +1215,48 @@ components: example: "A" dns_results: $ref: '#/components/schemas/DNSResults' + + BlacklistCheckRequest: + type: object + required: + - ip + properties: + ip: + type: string + description: IPv4 or IPv6 address to check against blacklists + example: "192.0.2.1" + pattern: '^([0-9]{1,3}\.){3}[0-9]{1,3}$|^([0-9a-fA-F]{0,4}:){7}[0-9a-fA-F]{0,4}$|^::([0-9a-fA-F]{0,4}:){0,6}[0-9a-fA-F]{0,4}$|^([0-9a-fA-F]{0,4}:){1,6}:([0-9a-fA-F]{0,4}:){0,5}[0-9a-fA-F]{0,4}$' + + BlacklistCheckResponse: + type: object + required: + - ip + - checks + - listed_count + - score + - grade + properties: + ip: + type: string + description: The IP address that was checked + example: "192.0.2.1" + checks: + type: array + items: + $ref: '#/components/schemas/BlacklistCheck' + description: List of blacklist check results + listed_count: + type: integer + description: Number of blacklists that have this IP listed + example: 0 + score: + type: integer + minimum: 0 + maximum: 100 + description: Blacklist score (0-100, higher is better) + example: 100 + grade: + type: string + enum: [A+, A, B, C, D, E, F] + description: Letter grade representation of the score + example: "A+" diff --git a/internal/api/handlers.go b/internal/api/handlers.go index fd57579..80c8f9a 100644 --- a/internal/api/handlers.go +++ b/internal/api/handlers.go @@ -41,6 +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, listedCount int, score int, grade string, err error) } // APIHandler implements the ServerInterface for handling API requests @@ -341,3 +342,41 @@ func (h *APIHandler) TestDomain(c *gin.Context) { c.JSON(http.StatusOK, response) } + +// CheckBlacklist checks an IP address against DNS blacklists +// (POST /blacklist) +func (h *APIHandler) CheckBlacklist(c *gin.Context) { + var request BlacklistCheckRequest + + // Bind and validate request + if err := c.ShouldBindJSON(&request); err != nil { + c.JSON(http.StatusBadRequest, Error{ + Error: "invalid_request", + Message: "Invalid request body", + Details: stringPtr(err.Error()), + }) + return + } + + // Perform blacklist check using analyzer + checks, listedCount, score, grade, err := h.analyzer.CheckBlacklistIP(request.Ip) + if err != nil { + c.JSON(http.StatusBadRequest, Error{ + Error: "invalid_ip", + Message: "Invalid IP address", + Details: stringPtr(err.Error()), + }) + return + } + + // Build response + response := BlacklistCheckResponse{ + Ip: request.Ip, + Checks: checks, + ListedCount: listedCount, + Score: score, + Grade: BlacklistCheckResponseGrade(grade), + } + + c.JSON(http.StatusOK, response) +} diff --git a/pkg/analyzer/analyzer.go b/pkg/analyzer/analyzer.go index 1cc5bf1..e7ae561 100644 --- a/pkg/analyzer/analyzer.go +++ b/pkg/analyzer/analyzer.go @@ -119,3 +119,23 @@ func (a *APIAdapter) AnalyzeDomain(domain string) (*api.DNSResults, int, string) return dnsResults, score, grade } + +// 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, 0, 0, "", err + } + + // Calculate score using the existing function + // Create a minimal RBLResults structure for scoring + results := &RBLResults{ + Checks: map[string][]api.BlacklistCheck{ip: checks}, + IPsChecked: []string{ip}, + ListedCount: listedCount, + } + score, grade := a.analyzer.generator.rblChecker.CalculateRBLScore(results) + + return checks, listedCount, score, grade, nil +} diff --git a/pkg/analyzer/rbl.go b/pkg/analyzer/rbl.go index 5e8b503..5fcb939 100644 --- a/pkg/analyzer/rbl.go +++ b/pkg/analyzer/rbl.go @@ -108,6 +108,28 @@ func (r *RBLChecker) CheckEmail(email *EmailMessage) *RBLResults { return results } +// 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) + } + + var checks []api.BlacklistCheck + listedCount := 0 + + // Check the IP against all RBLs + for _, rbl := range r.RBLs { + check := r.checkIP(ip, rbl) + checks = append(checks, check) + if check.Listed { + listedCount++ + } + } + + return checks, listedCount, nil +} + // extractIPs extracts IP addresses from Received headers func (r *RBLChecker) extractIPs(email *EmailMessage) []string { var ips []string diff --git a/web/routes.go b/web/routes.go index c60cb11..44b1cb2 100644 --- a/web/routes.go +++ b/web/routes.go @@ -63,6 +63,10 @@ func DeclareRoutes(cfg *config.Config, router *gin.Engine) { appConfig["survey_url"] = cfg.SurveyURL.String() } + if len(cfg.Analysis.RBLs) > 0 { + appConfig["rbls"] = cfg.Analysis.RBLs + } + if appcfg, err := json.MarshalIndent(appConfig, "", " "); err != nil { log.Println("Unable to generate JSON config to inject in web application") } else { diff --git a/web/src/lib/stores/config.ts b/web/src/lib/stores/config.ts index 4187307..8a978e0 100644 --- a/web/src/lib/stores/config.ts +++ b/web/src/lib/stores/config.ts @@ -29,6 +29,7 @@ interface AppConfig { const defaultConfig: AppConfig = { report_retention: 0, survey_url: "", + rbls: [], }; function getConfigFromScriptTag(): AppConfig | null { diff --git a/web/src/routes/+page.svelte b/web/src/routes/+page.svelte index 2cf556b..d3f17a3 100644 --- a/web/src/routes/+page.svelte +++ b/web/src/routes/+page.svelte @@ -235,9 +235,13 @@
- + - Or Test Domain Only + Test Domain Only + + + + Check IP Blacklist
diff --git a/web/src/routes/blacklist/+page.svelte b/web/src/routes/blacklist/+page.svelte new file mode 100644 index 0000000..f104e73 --- /dev/null +++ b/web/src/routes/blacklist/+page.svelte @@ -0,0 +1,186 @@ + + + + Blacklist Check - happyDeliver + + +
+
+
+ +
+

+ + Check IP Blacklist Status +

+

+ Test an IP address against multiple DNS-based blacklists (RBLs) to check its reputation. +

+
+ + +
+
+

Enter IP Address

+
+ + + + + +
+ + {#if error} + + {/if} + + + + Enter an IPv4 address (e.g., 192.0.2.1) or IPv6 address (e.g., 2001:db8::1) + +
+
+ + +
+
+
+
+

+ + What's Checked +

+
    + {#each $appConfig.rbls as rbl} +
  • {rbl}
  • + {/each} +
+
+
+
+ +
+
+
+

+ + Why Check Blacklists? +

+

+ DNS-based blacklists (RBLs) are used by email servers to identify and block spam sources. Being listed can severely impact email deliverability. +

+

+ This tool checks your IP against multiple popular RBLs to help you: +

+
    +
  • + Monitor IP reputation +
  • +
  • + Identify deliverability issues +
  • +
  • + Take corrective action +
  • +
+
+
+
+
+ + +
+

+ + Need Complete Email Analysis? +

+

+ For comprehensive deliverability testing including DKIM verification, content analysis, spam scoring, and more: +

+ + + Send Test Email + +
+
+
+
+ + diff --git a/web/src/routes/blacklist/[ip]/+page.svelte b/web/src/routes/blacklist/[ip]/+page.svelte new file mode 100644 index 0000000..4556552 --- /dev/null +++ b/web/src/routes/blacklist/[ip]/+page.svelte @@ -0,0 +1,230 @@ + + + + {ip} - Blacklist Check - happyDeliver + + +
+
+
+ +
+
+

+ + Blacklist Analysis +

+ + + Check Another IP + +
+
+ + {#if loading} + +
+
+
+ Loading... +
+

Checking {ip}...

+

Querying DNS-based blacklists

+
+
+ {:else if error} + +
+
+ +

Check Failed

+

{error}

+ +
+
+ {:else if result} + +
+ +
+
+
+
+

+ {result.ip} +

+ {#if result.listed_count === 0} +
+ + Not Listed +

+ This IP address is not listed on any checked blacklists. +

+
+ {:else} +
+ + Listed on {result.listed_count} blacklist{result.listed_count > 1 ? "s" : ""} +

+ This IP address is listed on {result.listed_count} of {result.checks.length} checked blacklist{result.checks.length > 1 ? "s" : ""}. +

+
+ {/if} +
+
+
+ + Blacklist Score +
+
+
+
+
+ + + + + +
+
+

+ + What This Means +

+ {#if result.listed_count === 0} +

+ Good news! This IP address is not currently listed on any of the + checked DNS-based blacklists (RBLs). This indicates a good sender reputation + and should not negatively impact email deliverability. +

+ {:else} +

+ Warning: This IP address is listed on {result.listed_count} blacklist{result.listed_count > 1 ? "s" : ""}. + Being listed can significantly impact email deliverability as many mail servers + use these blacklists to filter incoming mail. +

+
+

Recommended Actions:

+
    +
  • Investigate the cause of the listing (compromised system, spam complaints, etc.)
  • +
  • Fix any security issues or stop sending practices that led to the listing
  • +
  • Request delisting from each RBL (check their websites for removal procedures)
  • +
  • Monitor your IP reputation regularly to prevent future listings
  • +
+
+ {/if} +
+
+ + +
+
+

+ + Want Complete Email Analysis? +

+

+ This blacklist check tests IP reputation only. For comprehensive + deliverability testing including DKIM verification, content analysis, + spam scoring, and DNS configuration: +

+ + + Send Test Email + +
+
+
+ {/if} +
+
+
+ +