]> Humopery - dsnview.git/commitdiff
initial commit
authorErik Mackdanz <erikmack@gmail.com>
Sat, 14 Feb 2026 16:56:45 +0000 (10:56 -0600)
committerErik Mackdanz <erikmack@gmail.com>
Sat, 14 Feb 2026 16:56:45 +0000 (10:56 -0600)
README.md [new file with mode: 0644]
dsnview [new file with mode: 0755]

diff --git a/README.md b/README.md
new file mode 100644 (file)
index 0000000..6f79a26
--- /dev/null
+++ b/README.md
@@ -0,0 +1,11 @@
+This is a hastily vibe-coded script (thanks DeepSeek) that takes an
+email delivery status notification (DSN) on stdin, formats the report
+within to HTML and displays it with lynx. It's good to quickly view
+these report in mutt.
+
+Put it in your path with `chmod +x` and then add to your muttrc:
+
+```
+macro attach 'R' "<pipe-entry>dsnview<enter>"
+```
+
diff --git a/dsnview b/dsnview
new file mode 100755 (executable)
index 0000000..4b4bb51
--- /dev/null
+++ b/dsnview
@@ -0,0 +1,284 @@
+#!/bin/sh
+# Create temporary directory
+tmpdir=$(mktemp -d)
+
+# Set trap to clean up on exit
+cleanup() {
+    rm -rf "$tmpdir"
+}
+trap cleanup EXIT
+
+# Read stdin and save as attachment
+cat > "$tmpdir/attachment"
+
+# Determine file type and extract accordingly
+filetype=$(file -b "$tmpdir/attachment")
+
+case "$filetype" in
+    *"Zip archive"*)
+        echo "Detected ZIP archive"
+        unzip -q "$tmpdir/attachment" -d "$tmpdir"
+        ;;
+    *"gzip compressed"*)
+        echo "Detected gzip compressed file"
+        gunzip -c "$tmpdir/attachment" > "$tmpdir/extracted.xml" 2>/dev/null
+        ;;
+    *"XML"*)
+        echo "Detected XML file"
+        cp "$tmpdir/attachment" "$tmpdir/extracted.xml"
+        ;;
+    *)
+        echo "Unknown file type: $filetype"
+        exit 1
+        ;;
+esac
+
+# List contents
+echo "Extracted contents:"
+ls -la "$tmpdir"
+
+# Find and display the XML file
+xml_file=$(find "$tmpdir" -name "*.xml" -type f | head -1)
+if [ -n "$xml_file" ] && [ -f "$xml_file" ]; then
+    echo -e "\n=== Generating HTML DMARC Report for $(basename "$xml_file") ==="
+    
+    # Create colorful HTML XSL stylesheet
+    cat > "$tmpdir/dmarc-html.xsl" << 'EOF'
+<?xml version="1.0" encoding="UTF-8"?>
+<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+<xsl:output method="html" doctype-system="about:legacy-compat"/>
+<xsl:template match="/">
+<html>
+<head>
+    <title>DMARC Aggregate Report</title>
+    <style>
+        body { font-family: monospace; background: #0a0a0a; color: #e0e0e0; margin: 20px; }
+        h1 { color: #4fc3f7; border-bottom: 2px solid #4fc3f7; padding-bottom: 5px; }
+        h2 { color: #81c784; margin-top: 25px; }
+        .section { background: #1a1a1a; border: 1px solid #333; border-radius: 5px; padding: 15px; margin: 15px 0; }
+        .metadata { color: #bb86fc; }
+        .policy { color: #03dac6; }
+        .record { background: #252525; border-left: 4px solid #ff9800; padding: 10px; margin: 10px 0; }
+        .ip { color: #ff9800; font-weight: bold; }
+        .count { color: #f06292; }
+        .pass { color: #4caf50; }
+        .fail { color: #f44336; }
+        .neutral { color: #ffeb3b; }
+        .header { color: #64b5f6; }
+        table { border-collapse: collapse; width: 100%; margin: 10px 0; }
+        th { background: #333; color: #fff; padding: 8px; text-align: left; }
+        td { padding: 8px; border-bottom: 1px solid #444; }
+        tr:hover { background: #2a2a2a; }
+        .timestamp { color: #9e9e9e; font-size: 0.9em; }
+    </style>
+</head>
+<body>
+    <h1>📊 DMARC Aggregate Report</h1>
+    
+    <div class="section">
+        <h2>📋 Report Metadata</h2>
+        <table>
+            <tr><th>Field</th><th>Value</th></tr>
+            <tr><td>Organization</td><td class="metadata"><xsl:value-of select="feedback/report_metadata/org_name"/></td></tr>
+            <tr><td>Report ID</td><td class="metadata"><xsl:value-of select="feedback/report_metadata/report_id"/></td></tr>
+            <tr><td>Date Range</td><td class="metadata">
+                <xsl:value-of select="feedback/report_metadata/date_range/begin"/> to 
+                <xsl:value-of select="feedback/report_metadata/date_range/end"/>
+            </td></tr>
+            <tr><td>Contact Email</td><td class="metadata"><xsl:value-of select="feedback/report_metadata/email"/></td></tr>
+        </table>
+    </div>
+    
+    <div class="section">
+        <h2>⚙️ Published DMARC Policy</h2>
+        <table>
+            <tr><th>Field</th><th>Value</th></tr>
+            <tr><td>Domain</td><td class="policy"><xsl:value-of select="feedback/policy_published/domain"/></td></tr>
+            <tr><td>DKIM Alignment</td><td class="policy"><xsl:value-of select="feedback/policy_published/adkim"/></td></tr>
+            <tr><td>SPF Alignment</td><td class="policy"><xsl:value-of select="feedback/policy_published/aspf"/></td></tr>
+            <tr><td>Policy</td><td class="policy"><xsl:value-of select="feedback/policy_published/p"/></td></tr>
+            <tr><td>Subdomain Policy</td><td class="policy"><xsl:value-of select="feedback/policy_published/sp"/></td></tr>
+            <tr><td>Percentage</td><td class="policy"><xsl:value-of select="feedback/policy_published/pct"/>%</td></tr>
+        </table>
+    </div>
+    
+    <div class="section">
+        <h2>📨 Email Records</h2>
+        <xsl:for-each select="feedback/record">
+            <div class="record">
+                <h3>📧 Record <xsl:value-of select="position()"/></h3>
+                <table>
+                    <tr><td><strong>Source IP:</strong></td><td class="ip"><xsl:value-of select="row/source_ip"/></td></tr>
+                    <tr><td><strong>Message Count:</strong></td><td class="count"><xsl:value-of select="row/count"/></td></tr>
+                    <tr><td><strong>Header From:</strong></td><td class="header"><xsl:value-of select="identifiers/header_from"/></td></tr>
+                </table>
+                
+                <h4>Policy Evaluation:</h4>
+                <table>
+                    <tr>
+                        <th>Disposition</th>
+                        <th>DKIM</th>
+                        <th>SPF</th>
+                    </tr>
+                    <tr>
+                        <td><span class="{row/policy_evaluated/disposition}"><xsl:value-of select="row/policy_evaluated/disposition"/></span></td>
+                        <td><span class="{row/policy_evaluated/dkim}"><xsl:value-of select="row/policy_evaluated/dkim"/></span></td>
+                        <td><span class="{row/policy_evaluated/spf}"><xsl:value-of select="row/policy_evaluated/spf"/></span></td>
+                    </tr>
+                </table>
+                
+                <h4>Authentication Results:</h4>
+                <table>
+                    <tr>
+                        <th>Type</th>
+                        <th>Domain</th>
+                        <th>Result</th>
+                        <th>Selector</th>
+                    </tr>
+                    <tr>
+                        <td>DKIM</td>
+                        <td><xsl:value-of select="auth_results/dkim/domain"/></td>
+                        <td><span class="{auth_results/dkim/result}"><xsl:value-of select="auth_results/dkim/result"/></span></td>
+                        <td><xsl:value-of select="auth_results/dkim/selector"/></td>
+                    </tr>
+                    <tr>
+                        <td>SPF</td>
+                        <td><xsl:value-of select="auth_results/spf/domain"/></td>
+                        <td><span class="{auth_results/spf/result}"><xsl:value-of select="auth_results/spf/result"/></span></td>
+                        <td>—</td>
+                    </tr>
+                </table>
+            </div>
+        </xsl:for-each>
+    </div>
+</body>
+</html>
+</xsl:template>
+</xsl:stylesheet>
+EOF
+
+    # Generate HTML using xsltproc (more reliable for HTML output)
+    if command -v xsltproc >/dev/null 2>&1; then
+        html_file="$tmpdir/dmarc-report.html"
+        if xsltproc -o "$html_file" "$tmpdir/dmarc-html.xsl" "$xml_file"; then
+            echo "HTML report generated: $html_file"
+            
+            # Display with lynx if available
+            if command -v lynx >/dev/null 2>&1; then
+                echo -e "\n=== Displaying with lynx (press 'q' to quit) ==="
+                lynx "$html_file"
+            else
+                echo "lynx not found. HTML file saved to: $html_file"
+                echo "You can view it with: cat $html_file"
+            fi
+        else
+            echo "xsltproc failed, showing raw XML instead:" && cat "$xml_file"
+        fi
+    else
+        echo "xsltproc not found, showing raw XML:"
+        cat "$xml_file"
+    fi
+else
+    echo "No XML file found in extracted contents."
+fi
+
+# Sample XML:
+
+# <?xml version="1.0" encoding="UTF-8" ?>
+# <feedback>
+#   <version>1.0</version>
+#   <report_metadata>
+#     <org_name>google.com</org_name>
+#     <email>noreply-dmarc-support@google.com</email>
+#     <extra_contact_info>https://support.google.com/a/answer/2466580</extra_contact_info>
+#     <report_id>9340001457015044932</report_id>
+#     <date_range>
+#       <begin>1770768000</begin>
+#       <end>1770854399</end>
+#     </date_range>
+#   </report_metadata>
+#   <policy_published>
+#     <domain>atxsra.org</domain>
+#     <adkim>r</adkim>
+#     <aspf>r</aspf>
+#     <p>none</p>
+#     <sp>none</sp>
+#     <pct>100</pct>
+#     <np>none</np>
+#   </policy_published>
+#   <record>
+#     <row>
+#       <source_ip>173.255.193.22</source_ip>
+#       <count>16</count>
+#       <policy_evaluated>
+#         <disposition>none</disposition>
+#         <dkim>pass</dkim>
+#         <spf>pass</spf>
+#       </policy_evaluated>
+#     </row>
+#     <identifiers>
+#       <header_from>atxsra.org</header_from>
+#     </identifiers>
+#     <auth_results>
+#       <dkim>
+#         <domain>atxsra.org</domain>
+#         <result>pass</result>
+#         <selector>lin</selector>
+#       </dkim>
+#       <spf>
+#         <domain>atxsra.org</domain>
+#         <result>pass</result>
+#       </spf>
+#     </auth_results>
+#   </record>
+#   <record>
+#     <row>
+#       <source_ip>2600:3c00::f03c:95ff:fee2:e488</source_ip>
+#       <count>23</count>
+#       <policy_evaluated>
+#         <disposition>none</disposition>
+#         <dkim>pass</dkim>
+#         <spf>pass</spf>
+#       </policy_evaluated>
+#     </row>
+#     <identifiers>
+#       <header_from>atxsra.org</header_from>
+#     </identifiers>
+#     <auth_results>
+#       <dkim>
+#         <domain>atxsra.org</domain>
+#         <result>pass</result>
+#         <selector>lin</selector>
+#       </dkim>
+#       <spf>
+#         <domain>atxsra.org</domain>
+#         <result>pass</result>
+#       </spf>
+#     </auth_results>
+#   </record>
+#   <record>
+#     <row>
+#       <source_ip>209.85.220.41</source_ip>
+#       <count>3</count>
+#       <policy_evaluated>
+#         <disposition>none</disposition>
+#         <dkim>pass</dkim>
+#         <spf>fail</spf>
+#       </policy_evaluated>
+#     </row>
+#     <identifiers>
+#       <header_from>atxsra.org</header_from>
+#     </identifiers>
+#     <auth_results>
+#       <dkim>
+#         <domain>atxsra.org</domain>
+#         <result>pass</result>
+#         <selector>lin</selector>
+#       </dkim>
+#       <spf>
+#         <domain>gmail.com</domain>
+#         <result>pass</result>
+#       </spf>
+#     </auth_results>
+#   </record>
+# </feedback>