Implement the POP3 UIDL command.
[mailpopbox.git] / pop3.go
1 package main
2
3 import (
4 "crypto/tls"
5 "errors"
6 "fmt"
7 "io"
8 "io/ioutil"
9 "net"
10 "os"
11 "path"
12
13 "github.com/uber-go/zap"
14
15 "src.bluestatic.org/mailpopbox/pop3"
16 )
17
18 func runPOP3Server(config Config, log zap.Logger) <-chan error {
19 server := pop3Server{
20 config: config,
21 rc: make(chan error),
22 log: log.With(zap.String("server", "pop3")),
23 }
24 go server.run()
25 return server.rc
26 }
27
28 type pop3Server struct {
29 config Config
30 rc chan error
31 log zap.Logger
32 }
33
34 func (server *pop3Server) run() {
35 for _, s := range server.config.Servers {
36 if err := os.Mkdir(s.MaildropPath, 0700); err != nil && !os.IsExist(err) {
37 server.log.Error("failed to open maildrop", zap.Error(err))
38 server.rc <- err
39 }
40 }
41
42 tlsConfig, err := server.config.GetTLSConfig()
43 if err != nil {
44 server.log.Error("failed to configure TLS", zap.Error(err))
45 server.rc <- err
46 return
47 }
48
49 addr := fmt.Sprintf(":%d", server.config.POP3Port)
50 server.log.Info("starting server", zap.String("address", addr))
51
52 var l net.Listener
53 if tlsConfig == nil {
54 l, err = net.Listen("tcp", addr)
55 } else {
56 l, err = tls.Listen("tcp", addr, tlsConfig)
57 }
58 if err != nil {
59 server.log.Error("listen", zap.Error(err))
60 server.rc <- err
61 return
62 }
63
64 for {
65 conn, err := l.Accept()
66 if err != nil {
67 server.log.Error("accept", zap.Error(err))
68 server.rc <- err
69 break
70 }
71
72 go pop3.AcceptConnection(conn, server, server.log)
73 }
74 }
75
76 func (server *pop3Server) Name() string {
77 return server.config.Hostname
78 }
79
80 func (server *pop3Server) OpenMailbox(user, pass string) (pop3.Mailbox, error) {
81 for _, s := range server.config.Servers {
82 if user == "mailbox@"+s.Domain && pass == s.MailboxPassword {
83 return server.openMailbox(s.MaildropPath)
84 }
85 }
86 return nil, errors.New("permission denied")
87 }
88
89 func (server *pop3Server) openMailbox(maildrop string) (*mailbox, error) {
90 files, err := ioutil.ReadDir(maildrop)
91 if err != nil {
92 // TODO: hide error, log instead
93 return nil, err
94 }
95
96 mb := &mailbox{
97 messages: make([]message, 0, len(files)),
98 }
99
100 i := 0
101 for _, file := range files {
102 if file.IsDir() {
103 continue
104 }
105
106 msg := message{
107 filename: path.Join(maildrop, file.Name()),
108 index: i,
109 size: file.Size(),
110 }
111 mb.messages = append(mb.messages, msg)
112 i++
113 }
114
115 return mb, nil
116 }
117
118 type mailbox struct {
119 messages []message
120 }
121
122 type message struct {
123 filename string
124 index int
125 size int64
126 deleted bool
127 }
128
129 func (m message) UniqueID() string {
130 l := len(m.filename)
131 return path.Base(m.filename[:l-len(".msg")])
132 }
133
134 func (m message) ID() int {
135 return m.index + 1
136 }
137
138 func (m message) Size() int {
139 return int(m.size)
140 }
141
142 func (m message) Deleted() bool {
143 return m.deleted
144 }
145
146 func (mb *mailbox) ListMessages() ([]pop3.Message, error) {
147 msgs := make([]pop3.Message, len(mb.messages))
148 for i := 0; i < len(mb.messages); i++ {
149 msgs[i] = &mb.messages[i]
150 }
151 return msgs, nil
152 }
153
154 func (mb *mailbox) GetMessage(id int) pop3.Message {
155 if id > len(mb.messages) {
156 return nil
157 }
158 return &mb.messages[id-1]
159 }
160
161 func (mb *mailbox) Retrieve(msg pop3.Message) (io.ReadCloser, error) {
162 filename := msg.(*message).filename
163 return os.Open(filename)
164 }
165
166 func (mb *mailbox) Delete(msg pop3.Message) error {
167 msg.(*message).deleted = true
168 return nil
169 }
170
171 func (mb *mailbox) Close() error {
172 for _, message := range mb.messages {
173 if message.deleted {
174 os.Remove(message.filename)
175 }
176 }
177 return nil
178 }
179
180 func (mb *mailbox) Reset() {
181 for _, message := range mb.messages {
182 message.deleted = false
183 }
184 }