1 module mail.pop3;
2 
3 import core.thread;
4 
5 import std.algorithm;
6 import std.conv;
7 import std.string;
8 import std.encoding;
9 import std.uri;
10 import std.exception;
11 import std.typecons;
12 
13 import mail.msg;
14 import mail.socket;
15 
16 struct Pop3Reply
17 {
18     string code;
19     string message;
20 
21     @property bool success() const
22     {
23         return code.startsWith("+OK");
24     }
25 
26     void toString(scope void delegate(const(char)[]) sink) const
27     {
28         sink(code);
29         sink(" ");
30         sink(message);
31     }
32 };
33 
34 class Pop3
35 {
36     private
37     {
38         Socket _sock;
39         string _lastCode;
40         string _lastData;
41     }
42 
43     @property ulong length()
44     {
45         if (_sock.isOpen)
46         {
47             _sock.send("STAT\n");
48             if (isOk)
49             {
50                 return (_lastData.split(" ")[0]).to!ulong;
51             }
52         }
53         return 0;
54     }
55 
56     @property ulong size()
57     {
58         if (_sock.isOpen)
59         {
60             _sock.send("STAT\n");
61             if (isOk)
62             {
63                 return (_lastData.split(" ")[1]).to!ulong;
64             }
65         }
66         return 0;
67     }
68 
69     this(string host, ushort port = 110)
70     {
71         _sock = new Socket(host, port);
72     }
73 
74     Pop3Reply connect()
75     {
76         if (_sock.connect())
77         {
78             parseReply();
79             return Pop3Reply(_lastCode, _lastData);
80         }
81         return Pop3Reply("-ERR");
82     }
83 
84     void disconnect()
85     {
86         _sock.disconnect();
87     }
88 
89     Pop3Reply auth(string username, string password)
90     {
91         auto r = query("USER " ~ username);
92         if (r.success)
93         {
94             r = query("PASS " ~ password);
95         }
96         return r;
97     }
98 
99     Pop3Reply list()
100     {
101         return query("LIST", true);
102     }
103 
104     Pop3Reply stat()
105     {
106         return query("STAT");
107     }
108 
109     Pop3Reply noop()
110     {
111         return query("NOOP");
112     }
113 
114     Pop3Reply rset()
115     {
116         return query("RSET");
117     }
118 
119     Pop3Reply retr(ulong id)
120     {
121         return query("RETR " ~ id.to!string, true);
122     }
123 
124     Msg retrMsg(ulong id, bool canonize = true)
125     {
126         Msg m;
127         _sock.send("RETR " ~ id.to!string ~ "\n");
128         parseReply(true);
129         if (_lastCode.startsWith("+OK"))
130         {
131             auto mData = _lastData.findSplit("\n")[2].strip;
132             if (canonize)
133             {
134                 mData = mData.replace("\n..", "\n.");
135             }
136             import std.stdio;
137 
138             m = Msg.parse(cast(ubyte[]) mData);
139         }
140         return m;
141     }
142 
143     Headers getHeaders(ulong id)
144     {
145         _sock.send("TOP " ~ id.to!string ~ " 0\n");
146         parseReply(true);
147 
148         auto mData = _lastData.findSplit("\n")[2].strip;
149         mData = mData.replace("\n..", "\n.");
150 
151         Msg m = Msg.parse(cast(ubyte[]) mData);
152         return m.headers;
153     }
154 
155     Tuple!(ulong, string)[] getUIDLs()
156     {
157         import std.array : array;
158 
159         query("UIDL", true);
160         if (!_lastCode.startsWith("+OK"))
161             return [];
162         return _lastData.split("\r\n").filter!(s => s.length > 2).map!((s) {
163             auto a = s.split(" ");
164             return tuple(a[0].to!ulong, a[1]);
165         }).array;
166     }
167 
168     Pop3Reply dele(ulong id)
169     {
170         return query("DELE " ~ id.to!string);
171     }
172 
173     Pop3Reply quit()
174     {
175         return query("QUIT");
176     }
177 
178 private:
179     void parseReply(bool multiline = false)
180     {
181         auto tmp = recvAll(multiline).findSplit(multiline ? "\r\n" : " ");
182         _lastCode = tmp[0];
183         _lastData = tmp[2];
184     }
185 
186     bool isOk()
187     {
188         parseReply();
189         return _lastCode == "+OK";
190     }
191 
192     Pop3Reply query(string command, bool multiline = false)
193     {
194         if (!_sock.isOpen)
195             return Pop3Reply("-ERR");
196         _sock.send(command ~ "\n");
197         parseReply(multiline);
198         return Pop3Reply(_lastCode, _lastData);
199     }
200 
201     string recvAll(bool multiline = false)
202     {
203         string end = multiline ? "\r\n.\r\n" : "\r\n";
204         string result;
205         ptrdiff_t cnt = 0;
206         do
207         {
208             auto tmp = _sock.receive();
209             if (!tmp.length)
210                 break;
211             result ~= tmp;
212             Thread.sleep(dur!"msecs"(1));
213         }
214         while (!result.endsWith(end));
215 
216         if (!result.length)
217             return result;
218 
219         return result[0 .. $ - end.length];
220     }
221 }
222 
223 unittest
224 {
225     auto reply = Pop3Reply("+OK", "Test\r\n");
226     assert(reply.to!string == "+OK Test\r\n");
227 }