Handle local postfix delivery

This commit is contained in:
nemunaire 2025-10-24 10:12:43 +07:00
commit 4bbba66a81
3 changed files with 38 additions and 13 deletions

View file

@ -450,12 +450,19 @@ func (h *HeaderAnalyzer) parseReceivedHeader(receivedValue string) *api.Received
// Normalize whitespace - Received headers can span multiple lines // Normalize whitespace - Received headers can span multiple lines
normalized := strings.Join(strings.Fields(receivedValue), " ") normalized := strings.Join(strings.Fields(receivedValue), " ")
// Extract "from" field // Check if this is a "by-first" header (e.g., "by hostname (Postfix, from userid...)")
fromRegex := regexp.MustCompile(`(?i)from\s+([^\s(]+)`) // vs standard "from-first" header (e.g., "from hostname ... by hostname")
isByFirst := regexp.MustCompile(`^by\s+`).MatchString(strings.TrimSpace(normalized))
// Extract "from" field - only if not in "by-first" format
// Avoid matching "from" inside parentheses after "by"
if !isByFirst {
fromRegex := regexp.MustCompile(`(?i)^from\s+([^\s(]+)`)
if matches := fromRegex.FindStringSubmatch(normalized); len(matches) > 1 { if matches := fromRegex.FindStringSubmatch(normalized); len(matches) > 1 {
from := matches[1] from := matches[1]
hop.From = &from hop.From = &from
} }
}
// Extract "by" field // Extract "by" field
byRegex := regexp.MustCompile(`(?i)by\s+([^\s(]+)`) byRegex := regexp.MustCompile(`(?i)by\s+([^\s(]+)`)
@ -466,14 +473,16 @@ func (h *HeaderAnalyzer) parseReceivedHeader(receivedValue string) *api.Received
// Extract "with" field (protocol) - must come after "by" and before "id" or "for" // Extract "with" field (protocol) - must come after "by" and before "id" or "for"
// This ensures we get the mail transfer protocol, not other "with" occurrences // This ensures we get the mail transfer protocol, not other "with" occurrences
withRegex := regexp.MustCompile(`(?i)by\s+[^\s(]+[^;]*?\s+with\s+([A-Z0-9]+)`) // Avoid matching "with" inside parentheses (like in TLS details)
withRegex := regexp.MustCompile(`(?i)by\s+[^\s(]+[^;]*?\s+with\s+([A-Z0-9]+)(?:\s|;)`)
if matches := withRegex.FindStringSubmatch(normalized); len(matches) > 1 { if matches := withRegex.FindStringSubmatch(normalized); len(matches) > 1 {
with := matches[1] with := matches[1]
hop.With = &with hop.With = &with
} }
// Extract "id" field // Extract "id" field - should come after "with" or "by", not inside parentheses
idRegex := regexp.MustCompile(`(?i)id\s+([^\s;]+)`) // Match pattern: "id <value>" where value doesn't contain parentheses or semicolons
idRegex := regexp.MustCompile(`(?i)\s+id\s+([^\s;()]+)`)
if matches := idRegex.FindStringSubmatch(normalized); len(matches) > 1 { if matches := idRegex.FindStringSubmatch(normalized); len(matches) > 1 {
id := matches[1] id := matches[1]
hop.Id = &id hop.Id = &id

View file

@ -619,6 +619,16 @@ func TestParseReceivedHeader(t *testing.T) {
expectIp: nil, expectIp: nil,
expectHasTs: true, expectHasTs: true,
}, },
{
name: "Postfix local delivery with userid",
receivedValue: "by grunt.ycc.fr (Postfix, from userid 1000) id 67276801A8; Fri, 24 Oct 2025 04:17:25 +0200 (CEST)",
expectFrom: nil,
expectBy: strPtr("grunt.ycc.fr"),
expectWith: nil,
expectId: strPtr("67276801A8"),
expectIp: nil,
expectHasTs: true,
},
} }
analyzer := NewHeaderAnalyzer() analyzer := NewHeaderAnalyzer()

View file

@ -17,20 +17,26 @@
<div class="d-flex w-100 justify-content-between"> <div class="d-flex w-100 justify-content-between">
<h6 class="mb-1"> <h6 class="mb-1">
<span class="badge bg-primary me-2">{receivedChain.length - i}</span> <span class="badge bg-primary me-2">{receivedChain.length - i}</span>
{hop.reverse || '-'} <span class="text-muted">({hop.ip})</span>{hop.by || 'Unknown'} {hop.reverse || '-'}{#if hop.ip} <span class="text-muted">({hop.ip})</span>{/if}{hop.by || 'Unknown'}
</h6> </h6>
<small class="text-muted" title={hop.timestamp}>{hop.timestamp ? new Intl.DateTimeFormat('default', { dateStyle: 'long', 'timeStyle': 'short' }).format(new Date(hop.timestamp)) : '-'}</small> <small class="text-muted" title={hop.timestamp}>{hop.timestamp ? new Intl.DateTimeFormat('default', { dateStyle: 'long', 'timeStyle': 'short' }).format(new Date(hop.timestamp)) : '-'}</small>
</div> </div>
{#if hop.with || hop.id} {#if hop.with || hop.id}
<p class="mb-1 small"> <p class="mb-1 small d-flex gap-3">
{#if hop.with} {#if hop.with}
<span>
<span class="text-muted">Protocol:</span> <code>{hop.with}</code> <span class="text-muted">Protocol:</span> <code>{hop.with}</code>
</span>
{/if} {/if}
{#if hop.id} {#if hop.id}
<span class="text-muted ms-3">ID:</span> <code>{hop.id}</code> <span>
<span class="text-muted">ID:</span> <code>{hop.id}</code>
</span>
{/if} {/if}
{#if hop.from} {#if hop.from}
<span class="text-muted ms-3">Helo:</span> <code>{hop.from}</code> <span>
<span class="text-muted">Helo:</span> <code>{hop.from}</code>
</span>
{/if} {/if}
</p> </p>
{/if} {/if}