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 <vladimir@thecybershadow.net> 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 enum State 32 { 33 none, 34 connecting, 35 greeting, 36 hello, 37 mailFrom, 38 rcptTo, 39 data, 40 sendingData, 41 quit, 42 done, 43 error 44 } 45 46 @property State state() { return _state; } 47 48 this(Logger log, string localDomain, string server, ushort port = 25) 49 { 50 this.log = log; 51 this.localDomain = localDomain; 52 this.server = server; 53 this.port = port; 54 } 55 56 void delegate() handleSent; 57 void delegate() handleStateChanged; 58 void delegate(string message) handleError; 59 60 void sendMessage(string from, string to, string[] data) 61 { 62 assert(state == State.none || state == State.done, "SmtpClient busy"); 63 this.from = from; 64 this.to = to; 65 this.data = data; 66 connect(); 67 } 68 69 private: 70 string localDomain, server, from, to; 71 ushort port; 72 string[] data; 73 State _state; 74 75 @property void state(State state) 76 { 77 _state = state; 78 if (handleStateChanged) 79 handleStateChanged(); 80 } 81 82 void connect() 83 { 84 auto tcp = new TcpConnection(); 85 IConnection c = tcp; 86 87 c = lineAdapter = new LineBufferedAdapter(c); 88 89 TimeoutAdapter timer; 90 c = timer = new TimeoutAdapter(c); 91 timer.setIdleTimeout(60.seconds); 92 93 conn = c; 94 95 conn.handleConnect = &onConnect; 96 conn.handleDisconnect = &onDisconnect; 97 conn.handleReadData = &onReadData; 98 99 log("* Connecting to " ~ server ~ "..."); 100 state = State.connecting; 101 tcp.connect(server, port); 102 } 103 104 /// Socket connection. 105 LineBufferedAdapter lineAdapter; 106 IConnection conn; 107 108 /// Protocol log. 109 Logger log; 110 111 void onConnect() 112 { 113 log("* Connected, waiting for greeting..."); 114 state = State.greeting; 115 } 116 117 void onDisconnect(string reason, DisconnectType type) 118 { 119 log("* Disconnected (" ~ reason ~ ")"); 120 if (state < State.quit) 121 { 122 if (handleError) 123 handleError(reason); 124 return; 125 } 126 127 state = State.done; 128 129 if (handleSent) 130 handleSent(); 131 } 132 133 void sendLine(string line) 134 { 135 log("< " ~ line); 136 lineAdapter.send(line); 137 } 138 139 void onReadData(Data data) 140 { 141 auto line = cast(string)data.toHeap(); 142 log("> " ~ line); 143 try 144 handleLine(line); 145 catch (Exception e) 146 { 147 foreach (eLine; e.toString().splitLines()) 148 log("* " ~ eLine); 149 conn.disconnect("Error (%s) while handling line from SMTP server: %s".format(e.msg, line)); 150 } 151 } 152 153 void handleLine(string line) 154 { 155 string codeStr; 156 list(codeStr, null, line) = line.findSplit(" "); 157 auto code = codeStr.to!int; 158 159 switch (state) 160 { 161 case State.greeting: 162 enforce(code == 220, "Unexpected greeting"); 163 state = State.hello; 164 sendLine("HELO " ~ localDomain); 165 break; 166 case State.hello: 167 enforce(code == 250, "Unexpected HELO response"); 168 state = State.mailFrom; 169 sendLine("MAIL FROM: " ~ from); 170 break; 171 case State.mailFrom: 172 enforce(code == 250, "Unexpected MAIL FROM response"); 173 state = State.rcptTo; 174 sendLine("RCPT TO: " ~ to); 175 break; 176 case State.rcptTo: 177 enforce(code == 250, "Unexpected MAIL FROM response"); 178 state = State.data; 179 sendLine("DATA"); 180 break; 181 case State.data: 182 enforce(code == 354, "Unexpected DATA response"); 183 state = State.sendingData; 184 foreach (dataLine; data) 185 { 186 if (dataLine.startsWith(".")) 187 dataLine = "." ~ dataLine; 188 sendLine(dataLine); 189 } 190 sendLine("."); 191 break; 192 case State.sendingData: 193 enforce(code == 250, "Unexpected data response"); 194 state = State.quit; 195 sendLine("QUIT"); 196 break; 197 case State.quit: 198 enforce(code == 221, "Unexpected QUIT response"); 199 conn.disconnect("All done!"); 200 break; 201 default: 202 enforce(false, "Unexpected line in state " ~ text(state)); 203 } 204 } 205 }