1 /** 2 * Simple SMTP client. 3 * 4 * License: 5 * This Source Code Form is subject to the terms of 6 * the Mozilla Public License, v. 2.0. If a copy of 7 * the MPL was not distributed with this file, You 8 * can obtain one at http://mozilla.org/MPL/2.0/. 9 * 10 * Authors: 11 * Vladimir Panteleev <ae@cy.md> 12 */ 13 14 module ae.net.smtp.client; 15 16 import std.algorithm; 17 import std.conv; 18 import std.exception; 19 import std.string; 20 21 import core.time; 22 23 import ae.net.asockets; 24 import ae.sys.log; 25 import ae.sys.timing; 26 import ae.utils.array; 27 28 /// One SmtpClient instance connects, sends one message, and disconnects. 29 class SmtpClient 30 { 31 /// Current connection state. 32 enum State 33 { 34 none, /// 35 connecting, /// 36 greeting, /// 37 hello, /// 38 mailFrom, /// 39 rcptTo, /// 40 data, /// 41 sendingData, /// 42 quit, /// 43 done, /// 44 error, /// 45 } 46 47 @property State state() { return _state; } /// ditto 48 49 this(Logger log, string localDomain, string server, ushort port = 25) 50 { 51 this.log = log; 52 this.localDomain = localDomain; 53 this.server = server; 54 this.port = port; 55 } /// 56 57 void delegate() handleSent; /// Called when the message is successfully sent. 58 void delegate() handleStateChanged; /// Called when `state` changes. 59 void delegate(string message) handleError; /// Called on error. 60 61 /// Send a message. 62 /// `from` and `to` are the envelope `MAIL FROM` and `RCPT TO` addresses. 63 void sendMessage(string from, string to, string[] data) 64 { 65 assert(state == State.none || state == State.done, "SmtpClient busy"); 66 this.from = from; 67 this.to = to; 68 this.data = data; 69 connect(); 70 } 71 72 private: 73 string localDomain, server, from, to; 74 ushort port; 75 string[] data; 76 State _state; 77 78 @property void state(State state) 79 { 80 _state = state; 81 if (handleStateChanged) 82 handleStateChanged(); 83 } 84 85 void connect() 86 { 87 auto tcp = new TcpConnection(); 88 IConnection c = tcp; 89 90 c = lineAdapter = new LineBufferedAdapter(c); 91 92 TimeoutAdapter timer; 93 c = timer = new TimeoutAdapter(c); 94 timer.setIdleTimeout(60.seconds); 95 96 conn = c; 97 98 conn.handleConnect = &onConnect; 99 conn.handleDisconnect = &onDisconnect; 100 conn.handleReadData = &onReadData; 101 102 log("* Connecting to " ~ server ~ "..."); 103 state = State.connecting; 104 tcp.connect(server, port); 105 } 106 107 /// Socket connection. 108 LineBufferedAdapter lineAdapter; 109 IConnection conn; 110 111 /// Protocol log. 112 Logger log; 113 114 void onConnect() 115 { 116 log("* Connected, waiting for greeting..."); 117 state = State.greeting; 118 } 119 120 void onDisconnect(string reason, DisconnectType type) 121 { 122 log("* Disconnected (" ~ reason ~ ")"); 123 if (state < State.quit) 124 { 125 if (handleError) 126 handleError(reason); 127 return; 128 } 129 130 state = State.done; 131 132 if (handleSent) 133 handleSent(); 134 } 135 136 void sendLine(string line) 137 { 138 log("< " ~ line); 139 lineAdapter.send(line); 140 } 141 142 void onReadData(Data data) 143 { 144 string line = data.asDataOf!char.toGC(); 145 log("> " ~ line); 146 try 147 handleLine(line); 148 catch (Exception e) 149 { 150 foreach (eLine; e.toString().splitLines()) 151 log("* " ~ eLine); 152 conn.disconnect("Error (%s) while handling line from SMTP server: %s".format(e.msg, line)); 153 } 154 } 155 156 void handleLine(string line) 157 { 158 string codeStr; 159 list(codeStr, null, line) = line.findSplit(" "); 160 auto code = codeStr.to!int; 161 162 switch (state) 163 { 164 case State.greeting: 165 enforce(code == 220, "Unexpected greeting"); 166 state = State.hello; 167 sendLine("HELO " ~ localDomain); 168 break; 169 case State.hello: 170 enforce(code == 250, "Unexpected HELO response"); 171 state = State.mailFrom; 172 sendLine("MAIL FROM: " ~ from); 173 break; 174 case State.mailFrom: 175 enforce(code == 250, "Unexpected MAIL FROM response"); 176 state = State.rcptTo; 177 sendLine("RCPT TO: " ~ to); 178 break; 179 case State.rcptTo: 180 enforce(code == 250, "Unexpected MAIL FROM response"); 181 state = State.data; 182 sendLine("DATA"); 183 break; 184 case State.data: 185 enforce(code == 354, "Unexpected DATA response"); 186 state = State.sendingData; 187 foreach (dataLine; data) 188 { 189 if (dataLine.startsWith(".")) 190 dataLine = "." ~ dataLine; 191 sendLine(dataLine); 192 } 193 sendLine("."); 194 break; 195 case State.sendingData: 196 enforce(code == 250, "Unexpected data response"); 197 state = State.quit; 198 sendLine("QUIT"); 199 break; 200 case State.quit: 201 enforce(code == 221, "Unexpected QUIT response"); 202 conn.disconnect("All done!"); 203 break; 204 default: 205 enforce(false, "Unexpected line in state " ~ text(state)); 206 } 207 } 208 }