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 }