BillionMail icon indicating copy to clipboard operation
BillionMail copied to clipboard

Click Tracking - BillionMail shows internal port 5679 in email links despite reverse proxy configuration

Open bwalger opened this issue 4 months ago • 9 comments

Problem Description

When using BillionMail behind a reverse proxy (nginx Proxy Manager), the generated email links incorrectly include the internal port 5679 instead of using the external domain without port.

Current Behavior

  • Domain Settings configured as: https://example.com
  • Generated email links show: https://example.com:5679/tracking-link
  • Expected behavior: https://example.com/tracking-link

Environment

  • BillionMail version: 4.2.1
  • Setup: Docker with nginx Proxy Manager reverse proxy
  • External access: Port 443 (HTTPS)
  • Internal container port: 5679

Issues Identified

1. Port appears in email links

Even when Domain Settings are configured with the external domain, BillionMail appends the internal port to all tracking links in emails.

2. Reverse proxy headers ignored

Standard reverse proxy headers are being ignored:

proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port 443;
proxy_set_header X-Forwarded-Proto $scheme;

3. No option to disable click tracking

There doesn't appear to be any configuration option to completely disable click tracking for users who don't need this feature.

Proposed Solutions

Option 1: Respect reverse proxy headers

BillionMail should read X-Forwarded-Port and X-Forwarded-Proto headers to determine the correct external URL format.

Option 2: Enhanced Domain Settings

Allow Domain Settings to override port completely:

  • Current: Domain + internal port
  • Proposed: Use Domain Settings as-is without port modification

Option 3: Disable tracking option

Add configuration options to disable click tracking entirely:

  • Environment variable: DISABLE_CLICK_TRACKING=true
  • Admin UI toggle: "Enable Click Tracking" checkbox
  • Per-campaign option: Disable tracking for specific emails

Use Cases

  1. Corporate environments where tracking links look suspicious
  2. Privacy-focused setups where tracking is unwanted
  3. Reverse proxy deployments where internal ports shouldn't be exposed
  4. Mobile app deep links where tracking breaks Universal Links

Expected Behavior

  • Email links should respect external domain configuration
  • Option to completely disable click tracking
  • Proper reverse proxy header support
  • Clean URLs without internal port numbers

This issue affects professional email deployments and user experience. Would appreciate guidance on current workarounds or planned fixes.

bwalger avatar Aug 21 '25 08:08 bwalger

I'm encountering the same error, email links appear as mail.domain.com:4343/pmta/ instead of maill.domain.com/pmta/

fraschm1998 avatar Aug 21 '25 15:08 fraschm1998

Hey, please let me know what your current BillionMail version is.

dreambladeflag avatar Aug 22 '25 10:08 dreambladeflag

Hey @dreambladeflag - i am using version 4.2.1 - I just saw that you released 4.3 yesterday. Is problem fixed there?

Claude AI supposed following changes. Maybe they help.

Thanks

Summary

BillionMail currently has issues when deployed behind reverse proxies (nginx Proxy Manager, Apache, etc.) that result in malformed email tracking URLs containing internal ports. Additionally, there's no way to disable email tracking features. This proposal provides specific code changes to resolve these issues.

Problems

1. Internal Port Exposure in Email Links

Current behavior:

  • Email tracking links include internal ports: https://example.com:5679/pmta/tracking-code
  • This happens even when BILLIONMAIL_HOSTNAME is correctly set to https://example.com

Expected behavior:

  • Clean external URLs: https://example.com/pmta/tracking-code

2. Reverse Proxy Headers Ignored

Current behavior:

  • nginx Proxy Manager headers (X-Forwarded-Host, X-Forwarded-Proto, X-Forwarded-Port) are ignored
  • BillionMail always uses internal server configuration for URL generation

Expected behavior:

  • Automatic detection and use of reverse proxy headers
  • Proper scheme and host resolution from proxy headers

3. No Way to Disable Email Tracking

Current behavior:

  • Click tracking and open tracking (pixels) are always enabled
  • No configuration option to disable these features

Expected behavior:

  • Administrative controls to enable/disable click tracking and open tracking independently
  • API endpoints for programmatic control

Proposed Solution

Code Changes Required

1. Enhanced URL Generation (core/internal/service/domains/baseurl.go)

Add reverse proxy detection:

// Check if we're behind a reverse proxy - if so, don't append port
var reverseProxyDomain string
err := public.OptionsMgrInstance.GetOption(context.Background(), "reverse_proxy_domain", &reverseProxyDomain)
if err == nil && reverseProxyDomain != "" {
    withPort = false
}

Add header-based URL building:

// GetBaseURLFromRequest builds URL from reverse proxy headers if available
func GetBaseURLFromRequest(r *ghttp.Request) string {
    if r == nil {
        return GetBaseURL()
    }
    
    // Check for reverse proxy headers
    forwardedProto := r.Header.Get("X-Forwarded-Proto")
    forwardedHost := r.Header.Get("X-Forwarded-Host")
    forwardedPort := r.Header.Get("X-Forwarded-Port")
    
    if forwardedHost != "" {
        scheme := "https"
        if forwardedProto != "" {
            scheme = forwardedProto
        }
        
        // Build URL with proper port handling
        if forwardedPort != "" && forwardedPort != "80" && forwardedPort != "443" {
            if (scheme == "https" && forwardedPort != "443") || (scheme == "http" && forwardedPort != "80") {
                return scheme + "://" + forwardedHost + ":" + forwardedPort
            }
        }
        
        return scheme + "://" + forwardedHost
    }
    
    return GetBaseURL()
}

2. Email Tracking Configuration

API Structure (core/api/settings/v1/settings.go):

// Email tracking configuration
EmailTracking struct {
    ClickTrackingEnabled bool `json:"click_tracking_enabled" dc:"click tracking enabled"`
    OpenTrackingEnabled  bool `json:"open_tracking_enabled" dc:"open tracking enabled"`
} `json:"email_tracking" dc:"email tracking configuration"`

type SetEmailTrackingConfigReq struct {
    g.Meta              `path:"/settings/set_email_tracking_config" tags:"Settings" method:"post"`
    Authorization       string `json:"authorization" in:"header" v:"required"`
    ClickTrackingEnabled bool   `json:"click_tracking_enabled"`
    OpenTrackingEnabled  bool   `json:"open_tracking_enabled"`
}

Tracking Logic (core/internal/service/maillog_stat/tracker.go):

// TrackLinks - Check if click tracking is enabled
func (t *MailTracker) TrackLinks() {
    var clickTrackingEnabled bool
    err := public.OptionsMgrInstance.GetOption(context.Background(), "click_tracking_enabled", &clickTrackingEnabled)
    if err != nil {
        clickTrackingEnabled = true // Default to enabled
    }

    if !clickTrackingEnabled {
        return // Skip tracking if disabled
    }
    // ... existing logic
}

// AppendTrackingPixel - Check if open tracking is enabled
func (t *MailTracker) AppendTrackingPixel() {
    var openTrackingEnabled bool
    err := public.OptionsMgrInstance.GetOption(context.Background(), "open_tracking_enabled", &openTrackingEnabled)
    if err != nil {
        openTrackingEnabled = true // Default to enabled
    }

    if !openTrackingEnabled {
        return // Skip tracking if disabled
    }
    // ... existing logic
}

Files to Modify

  1. core/internal/service/domains/baseurl.go - URL generation logic
  2. core/api/settings/v1/settings.go - API definitions
  3. core/internal/controller/settings/settings_v1_get_system_config.go - Load tracking config
  4. core/internal/controller/settings/settings_v1_set_email_tracking_config.go - New API endpoint
  5. core/internal/service/maillog_stat/tracker.go - Respect tracking settings

Benefits

  1. Clean URLs: Email links will be properly formatted without internal ports
  2. Reverse Proxy Support: Full compatibility with nginx, Apache, Cloudflare, etc.
  3. Privacy Controls: Administrators can disable tracking for compliance (GDPR, etc.)
  4. Backward Compatibility: All existing functionality preserved
  5. API Integration: Programmatic control over tracking settings

Configuration Example

nginx Proxy Manager Headers:

proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Port 443;

API Usage:

# Set reverse proxy domain
POST /api/settings/set_reverse_proxy_domain
{"domain": "https://mail.example.com"}

# Disable click tracking, keep open tracking
POST /api/settings/set_email_tracking_config
{"click_tracking_enabled": false, "open_tracking_enabled": true}

bwalger avatar Aug 22 '25 10:08 bwalger

Hey, please let me know what your current BillionMail version is.

Just upgraded to 4.3.1 and still encountering the same error. Clicking email links appear as https://mail.example.com:4343/pmta/rFNdgvLFt6yLP1BxL... removing the :4343 redirects to the proper url.

Port Info:

## MAIL Ports
SMTP_PORT=25
SMTPS_PORT=465
SUBMISSION_PORT=587
IMAP_PORT=143
IMAPS_PORT=993
POP_PORT=110
POPS_PORT=995
REDIS_PORT=127.0.0.1:26379
SQL_PORT=127.0.0.1:25432

## Manage Ports
HTTP_PORT=81
HTTPS_PORT=4343

nginx config:

server {

    listen 80;
    server_name mail.example.com;
    return 301 https://$server_name$request_uri;

}

server {

    listen 443 ssl;
    server_name mail.example.com;
    # add Strict-Transport-Security to prevent man in the middle attacks
    add_header Strict-Transport-Security "max-age=31536000" always;

    set $upstream 0.0.0.0:81;

    location / {

        set_real_ip_from 0.0.0.0/0;
        real_ip_header X-Forwarded-For;

        proxy_pass_header Authorization;
        proxy_pass http://$upstream;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_buffering off;
        client_max_body_size 0;
        proxy_read_timeout 36000s;
        proxy_redirect off;
    }

    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;
    }

    ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem; # managed by Certbot
}

fraschm1998 avatar Aug 22 '25 16:08 fraschm1998

Thank you for your feedback. We have noticed this issue, and we'll fix it in the next release.

dreambladeflag avatar Aug 25 '25 03:08 dreambladeflag

Hey, are you using BillionMail reverse proxy in the Settings? Like this

Image

dreambladeflag avatar Aug 25 '25 04:08 dreambladeflag

Hey, are you using BillionMail reverse proxy in the Settings? Like this

Image

Odd I didn't even see that till now, can't remember setting it unless it was done automatically... I'll try removing the :4343

fraschm1998 avatar Aug 25 '25 04:08 fraschm1998

I tried updating it but get an xhr 500 internal error:

Image

fraschm1998 avatar Aug 25 '25 04:08 fraschm1998

Hey, are you using BillionMail reverse proxy in the Settings? Like this

Image

我也想停止追踪来着,主要目的是为了不显示实际域名。看到你这图有解了,十分感谢,已反代。

bihell avatar Oct 26 '25 22:10 bihell