Skip to content

Commit c6fd3b1

Browse files
committed
cmd/dmarc-milter: add new tool
1 parent 999f438 commit c6fd3b1

File tree

1 file changed

+221
-0
lines changed

1 file changed

+221
-0
lines changed

cmd/dmarc-milter/main.go

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
//+build ignore
2+
3+
package main
4+
5+
import (
6+
"flag"
7+
"log"
8+
"net"
9+
"net/textproto"
10+
"os"
11+
"os/signal"
12+
"strings"
13+
"syscall"
14+
15+
//"github.com/emersion/go-msgauth/dmarc"
16+
"github.com/emersion/go-milter"
17+
"github.com/emersion/go-msgauth/authres"
18+
)
19+
20+
var (
21+
identity string
22+
authServer string
23+
listenURI string
24+
)
25+
26+
func init() {
27+
flag.StringVar(&identity, "i", "", "Server identity (default: system hostname)")
28+
flag.StringVar(&authServer, "a", "", "Trusted authentication server (default: identity)")
29+
flag.StringVar(&listenURI, "l", "unix:///tmp/dkim-milter.sock", "Listen URI")
30+
flag.Parse()
31+
}
32+
33+
type session struct {
34+
authResDelete []int
35+
}
36+
37+
func (s *session) Connect(host string, family string, port uint16, addr net.IP, m *milter.Modifier) (milter.Response, error) {
38+
return nil, nil
39+
}
40+
41+
func (s *session) Helo(name string, m *milter.Modifier) (milter.Response, error) {
42+
return nil, nil
43+
}
44+
45+
func (s *session) MailFrom(from string, m *milter.Modifier) (milter.Response, error) {
46+
return nil, nil
47+
}
48+
49+
func (s *session) RcptTo(rcptTo string, m *milter.Modifier) (milter.Response, error) {
50+
return nil, nil
51+
}
52+
53+
func (s *session) Header(name string, value string, m *milter.Modifier) (milter.Response, error) {
54+
return milter.RespContinue, nil
55+
}
56+
57+
func parseAddressDomain(s string) (string, error) {
58+
addr, err := mail.ParseAddress(s)
59+
if err != nil {
60+
return "", err
61+
}
62+
63+
parts := strings.SplitN(addr.Address, "@", 2)
64+
if len(parts) != 2 {
65+
return "", fmt.Errorf("dmarc-milter: malformed address: missing '@'")
66+
}
67+
68+
return parts[1], nil
69+
}
70+
71+
func hasDMARC(results []authres.Result) bool {
72+
for _, res := range results {
73+
if _, ok := res.(*authres.DMARCResult); ok {
74+
return true
75+
}
76+
}
77+
return false
78+
}
79+
80+
func (s *session) processAuthRes(field string) error {
81+
id, results, err := authres.Parse(field)
82+
if err != nil {
83+
// Delete fields we can't parse, because other implementations might
84+
// accept malformed fields
85+
s.authResDelete = append(s.authResDelete, i)
86+
return nil
87+
}
88+
89+
if strings.EqualFold(id, identity) && hasDMARC(results) {
90+
// This is our Authentication-Results field, and it contains a DMARC
91+
// result. Delete the header field.
92+
s.authResDelete = append(s.authResDelete, i)
93+
return nil
94+
}
95+
96+
if strings.EqualFold(id, authServer) {
97+
// This is an Authentication-Results field we can trust
98+
99+
}
100+
101+
return nil
102+
}
103+
104+
func (s *session) evaluate(h textproto.MIMEHeader, m *milter.Modifier) (*authres.DMARCResult, error) {
105+
from := h.Get("From")
106+
if from == "" {
107+
return "", fmt.Errorf("dmarc-milter: missing From header field")
108+
}
109+
domain, err := parseAddressDomain(from)
110+
if err != nil {
111+
return "", fmt.Errorf("dmarc-milter: malformed From header field: %v", err)
112+
}
113+
114+
noneResult := &authres.DMARCResult{
115+
Result: authres.ResultNone,
116+
From: from,
117+
}
118+
119+
record, err := dmarc.Lookup(domain)
120+
if err == dmarc.ErrNoPolicy {
121+
// TODO: use golang.org/x/net/publicsuffix to query the top-level DMARC record
122+
return noneResult, nil
123+
} else if err != nil {
124+
return "", err
125+
}
126+
127+
fields := h["Authentication-Results"]
128+
for i, field := range fields {
129+
if err := s.processAuthRes(field, i); err != nil {
130+
return nil, err
131+
}
132+
133+
id, results, err := authres.Parse(field)
134+
if err != nil {
135+
}
136+
137+
// Delete any existing Authentication-Results header field with our identity
138+
if shouldDeleteAuthRes(field) {
139+
s.authResDelete = append(s.authResDelete, i)
140+
}
141+
}
142+
143+
return &authres.DMARCResult{
144+
Result: authres.ResultPass,
145+
From: from,
146+
}, nil
147+
}
148+
149+
func (s *session) Headers(h textproto.MIMEHeader, m *milter.Modifier) (milter.Response, error) {
150+
result, err := s.evaluate(h, m)
151+
if err != nil {
152+
if result == nil {
153+
result = &authres.Result{
154+
Result:
155+
From: h.Get("From"),
156+
}
157+
}
158+
}
159+
160+
return milter.RespContinue, nil
161+
}
162+
163+
func (s *session) BodyChunk(chunk []byte, m *milter.Modifier) (milter.Response, error) {
164+
return milter.RespContinue, nil
165+
}
166+
167+
func (s *session) Body(m *milter.Modifier) (milter.Response, error) {
168+
for _, index := range s.authResDelete {
169+
if err := m.ChangeHeader(index, "Authentication-Results", ""); err != nil {
170+
return nil, err
171+
}
172+
}
173+
174+
return milter.RespAccept, nil
175+
}
176+
177+
func main() {
178+
if identity == "" {
179+
var err error
180+
if identity, err = os.Hostname(); err != nil {
181+
log.Fatalf("Failed to get system hostname: %v", err)
182+
}
183+
}
184+
if authServer == "" {
185+
authServer = identity
186+
}
187+
188+
parts := strings.SplitN(listenURI, "://", 2)
189+
if len(parts) != 2 {
190+
log.Fatal("Invalid listen URI")
191+
}
192+
listenNetwork, listenAddr := parts[0], parts[1]
193+
194+
s := milter.Server{
195+
NewMilter: func() milter.Milter {
196+
return &session{}
197+
},
198+
Actions: milter.OptAddHeader | milter.OptChangeHeader,
199+
Protocol: milter.OptNoConnect | milter.OptNoHelo | milter.OptNoMailFrom | milter.OptNoRcptTo | milter.OptNoBody,
200+
}
201+
202+
ln, err := net.Listen(listenNetwork, listenAddr)
203+
if err != nil {
204+
log.Fatal("Failed to setup listener: ", err)
205+
}
206+
207+
// Closing the listener will unlink the unix socket, if any
208+
sigs := make(chan os.Signal, 1)
209+
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
210+
go func() {
211+
<-sigs
212+
if err := s.Close(); err != nil {
213+
log.Fatal("Failed to close server: ", err)
214+
}
215+
}()
216+
217+
log.Println("Milter listening at", listenURI)
218+
if err := s.Serve(ln); err != nil && err != milter.ErrServerClosed {
219+
log.Fatal("Failed to serve: ", err)
220+
}
221+
}

0 commit comments

Comments
 (0)