Move smtp.RelayMessage into a new smtp.MTA interface.
[mailpopbox.git] / smtp / server.go
1 // mailpopbox
2 // Copyright 2020 Blue Static <https://www.bluestatic.org>
3 // This program is free software licensed under the GNU General Public License,
4 // version 3.0. The full text of the license can be found in LICENSE.txt.
5 // SPDX-License-Identifier: GPL-3.0-only
6
7 package smtp
8
9 import (
10 "crypto/rand"
11 "crypto/tls"
12 "fmt"
13 "io"
14 "net"
15 "net/mail"
16 "regexp"
17 "strings"
18 "time"
19
20 "go.uber.org/zap"
21 )
22
23 type ReplyLine struct {
24 Code int
25 Message string
26 }
27
28 func (l ReplyLine) String() string {
29 return fmt.Sprintf("%d %s", l.Code, l.Message)
30 }
31
32 var SendAsSubject = regexp.MustCompile(`(?i)\[sendas:\s*([a-zA-Z0-9\.\-_]+)\]`)
33
34 var (
35 ReplyOK = ReplyLine{250, "OK"}
36 ReplyAuthOK = ReplyLine{235, "auth success"}
37 ReplyBadSyntax = ReplyLine{501, "syntax error"}
38 ReplyBadSequence = ReplyLine{503, "bad sequence of commands"}
39 ReplyBadMailbox = ReplyLine{550, "mailbox unavailable"}
40 ReplyMailboxUnallowed = ReplyLine{553, "mailbox name not allowed"}
41 )
42
43 func DomainForAddress(addr mail.Address) string {
44 return DomainForAddressString(addr.Address)
45 }
46
47 func DomainForAddressString(address string) string {
48 domainIdx := strings.LastIndex(address, "@")
49 if domainIdx == -1 {
50 return ""
51 }
52 return address[domainIdx+1:]
53 }
54
55 type Envelope struct {
56 RemoteAddr net.Addr
57 EHLO string
58 MailFrom mail.Address
59 RcptTo []mail.Address
60 Data []byte
61 Received time.Time
62 ID string
63 }
64
65 func WriteEnvelopeForDelivery(w io.Writer, e Envelope) {
66 fmt.Fprintf(w, "Delivered-To: <%s>\r\n", e.RcptTo[0].Address)
67 fmt.Fprintf(w, "Return-Path: <%s>\r\n", e.MailFrom.Address)
68 w.Write(e.Data)
69 }
70
71 func generateEnvelopeId(prefix string, t time.Time) string {
72 var idBytes [4]byte
73 rand.Read(idBytes[:])
74 return fmt.Sprintf("%s.%d.%x", prefix, t.UnixNano(), idBytes)
75 }
76
77 // lookupRemoteHost attempts to reverse look-up the provided IP address. On
78 // success, it returns the hostname and the IP as formatted for a receive
79 // trace. If the lookup fails, it just returns the original IP.
80 func lookupRemoteHost(addr net.Addr) string {
81 rhost, _, err := net.SplitHostPort(addr.String())
82 if err != nil {
83 rhost = addr.String()
84 }
85
86 rhosts, err := net.LookupAddr(rhost)
87 if err == nil {
88 rhost = fmt.Sprintf("%s [%s]", rhosts[0], rhost)
89 }
90
91 return rhost
92 }
93
94 // Server provides an interface for handling incoming SMTP requests via
95 // AcceptConnection.
96 type Server interface {
97 // Returns the name of the server, to use in HELO advertisements.
98 Name() string
99
100 // If non-nil, enables STARTTLS support on the SMTP server with the given
101 // configuration.
102 TLSConfig() *tls.Config
103
104 // Returns an status line indicating whether this server can receive
105 // mail for the specified email address.
106 VerifyAddress(mail.Address) ReplyLine
107
108 // Verify that the authc+passwd identity can send mail as authz on this
109 // server.
110 Authenticate(authz, authc, passwd string) bool
111
112 // Delivers a valid incoming message to a recipient on this server. The
113 // addressee has been validated via VerifyAddress.
114 DeliverMessage(Envelope) *ReplyLine
115
116 // RelayMessage instructs the server to send the Envelope to another
117 // MTA for outbound delivery.
118 RelayMessage(Envelope)
119 }
120
121 // MTA (Mail Transport Agent) allows a Server to interface with other SMTP
122 // MTAs.
123 type MTA interface {
124 // RelayMessage will attempt to send the specified Envelope. It will ask the
125 // Server to dial the MX servers for the addresses in Envelope.RcptTo for
126 // delivery. If relaying fails, a failure notice will be sent to the sender
127 // via Server.DeliverMessage.
128 RelayMessage(Envelope)
129 }
130
131 func NewDefaultMTA(server Server, log *zap.Logger) MTA {
132 return &mta{
133 server: server,
134 log: log,
135 }
136 }
137
138 type mta struct {
139 server Server
140 log *zap.Logger
141 }
142
143 type EmptyServerCallbacks struct{}
144
145 func (*EmptyServerCallbacks) TLSConfig() *tls.Config {
146 return nil
147 }
148
149 func (*EmptyServerCallbacks) VerifyAddress(mail.Address) ReplyLine {
150 return ReplyOK
151 }
152
153 func (*EmptyServerCallbacks) Authenticate(authz, authc, passwd string) bool {
154 return false
155 }
156
157 func (*EmptyServerCallbacks) DeliverMessage(Envelope) *ReplyLine {
158 return nil
159 }
160
161 func (*EmptyServerCallbacks) RelayMessage(Envelope) {
162 }