1 /++
2 	 A bare-bones, dead simple incoming SMTP server with zero outbound mail support. Intended for applications that want to process inbound email on a VM or something.
3 
4 
5 	 $(H2 Alternatives)
6 
7 	 You can also run a real email server and process messages as they are delivered with a biff notification or get them from imap or something too.
8 
9 	 History:
10 	 	Written December 26, 2020, in a little over one hour. Don't expect much from it!
11 +/
12 module arsd.mailserver;
13 
14 import arsd.fibersocket;
15 import arsd.email;
16 
17 ///
18 struct SmtpServerConfig {
19 	//string iface = null;
20 	ushort port = 25;
21 	string hostname;
22 }
23 
24 ///
25 void serveSmtp(FiberManager fm, SmtpServerConfig config, void delegate(string[] recipients, IncomingEmailMessage) handler) {
26 	fm.listenTcp4(config.port, (Socket socket) {
27 		ubyte[512] buffer;
28 		ubyte[] at;
29 		const(ubyte)[] readLine() {
30 			top:
31 			int index = -1;
32 			foreach(idx, b; at) {
33 				if(b == 10) {
34 					index = cast(int) idx;
35 					break;
36 				}
37 			}
38 			if(index != -1) {
39 				auto got = at[0 .. index];
40 				at = at[index + 1 .. $];
41 				if(got.length) {
42 					if(got[$-1] == '\n')
43 						got = got[0 .. $-1];
44 					if(got[$-1] == '\r')
45 						got = got[0 .. $-1];
46 				}
47 				return got;
48 			}
49 			if(at.ptr is buffer.ptr && at.length < buffer.length) {
50 				auto got = socket.receive(buffer[at.length .. $]);
51 				if(got < 0) {
52 					socket.close();
53 					return null;
54 				} if(got == 0) {
55 					socket.close();
56 					return null;
57 				} else {
58 					at = buffer[0 .. at.length + got];
59 					goto top;
60 				}
61 			} else {
62 				// no space
63 				if(at.ptr is buffer.ptr)
64 					at = at.dup;
65 
66 				auto got = socket.receive(buffer[]);
67 				if(got <= 0) {
68 					socket.close();
69 					return null;
70 				} else {
71 					at ~= buffer[0 .. got];
72 					goto top;
73 				}
74 			}
75 
76 			assert(0);
77 		}
78 
79 		socket.sendAll("220 " ~ config.hostname ~ " SMTP arsd_mailserver\r\n"); // ESMTP?
80 
81 		immutable(ubyte)[][] msgLines;
82 		string[] recipients;
83 
84 		loop: while(socket.isAlive()) {
85 			auto line = readLine();
86 			if(line is null) {
87 				socket.close();
88 				break;
89 			}
90 
91 			if(line.length < 4) {
92 				socket.sendAll("500 Unknown command");
93 				continue;
94 			}
95 
96 			switch(cast(string) line[0 .. 4]) {
97 				case "HELO":
98 					socket.sendAll("250 " ~ config.hostname ~ " Hello, good to see you\r\n");
99 				break;
100 				case "EHLO":
101 					goto default; // FIXME
102 				case "MAIL":
103 					// MAIL FROM:<email address>
104 					// 501 5.1.7 Syntax error in mailbox address "me@a?example.com.arsdnet.net" (non-printable character)
105 
106 					if(line.length < 11 || line[0 .. 10] != "MAIL FROM:") {
107 						socket.sendAll("501 Syntax error");
108 						continue;
109 					}
110 
111 					line = line[10 .. $];
112 					if(line[0] == '<') {
113 						if(line[$-1] != '>') {
114 							socket.sendAll("501 Syntax error");
115 							continue;
116 						}
117 
118 						line = line[1 .. $-1];
119 					}
120 
121 					string currentDate; // FIXME
122 					msgLines ~= cast(immutable(ubyte)[]) ("From " ~ cast(string) line ~ "  " ~ currentDate);
123 					msgLines ~= cast(immutable(ubyte)[]) ("Received: from " ~ socket.remoteAddress.toString);
124 
125 					socket.sendAll("250 OK\r\n");
126 				break;
127 				case "RCPT":
128 					// RCPT TO:<...>
129 
130 					if(line.length < 9 || line[0 .. 8] != "RCPT TO:") {
131 						socket.sendAll("501 Syntax error");
132 						continue;
133 					}
134 
135 					line = line[8 .. $];
136 					if(line[0] == '<') {
137 						if(line[$-1] != '>') {
138 							socket.sendAll("501 Syntax error");
139 							continue;
140 						}
141 
142 						line = line[1 .. $-1];
143 					}
144 
145 					recipients ~= (cast(char[]) line).idup;
146 
147 					socket.sendAll("250 OK\r\n");
148 				break;
149 				case "DATA":
150 					socket.sendAll("354 Enter mail, end with . on line by itself\r\n");
151 
152 					more_lines:
153 					line = readLine();
154 
155 					if(line == ".") {
156 						handler(recipients, new IncomingEmailMessage(msgLines));
157 						socket.sendAll("250 OK\r\n");
158 					} else if(line is null) {
159 						socket.close();
160 						break loop;
161 					} else {
162 						msgLines ~= line.idup;
163 						goto more_lines;
164 					}
165 				break;
166 				case "QUIT":
167 					socket.sendAll("221 Bye\r\n");
168 					socket.close();
169 				break;
170 				default:
171 					socket.sendAll("500 5.5.1 Command unrecognized\r\n");
172 			}
173 		}
174 	});
175 }
176 
177 version(Demo)
178 void main() {
179 	auto fm = new FiberManager;
180 
181 	fm.serveSmtp(SmtpServerConfig(9025), (string[] recipients, IncomingEmailMessage iem) {
182 		import std.stdio;
183 		writeln(recipients);
184 		writeln(iem.subject);
185 		writeln(iem.textMessageBody);
186 	});
187 
188 	fm.run;
189 }