1 module smtp.client;
2 
3 import std.algorithm;
4 import std.base64;
5 import std.conv;
6 import std.socket;
7 import std.stdio;
8 import std.string;
9 
10 import smtp.reply;
11 import smtp.ssl;
12 
13 /++
14  Authentication types supported by SMTP protocol.
15  +/
16 enum SmtpAuthType {
17 	PLAIN = 0,
18 	LOGIN,
19 };
20 
21 /++
22  Low-level synchronous API for implementing SMTP clients.
23 
24  SmtpClient handles:
25   + TCP data transmission and buffers management
26   + TLS/SSL channel encryption
27   + SMTP protocol request/reply handling
28   + transport layer exceptions
29 
30  SmtpClient does NOT handle:
31   * semantic class usage mistakes like bad commands sequences
32 
33  Supported SMTP commands:
34   NOOP, HELO, EHLO, MAIL, RSET, RCPT, DATA (data, dataBody), AUTH (auth, authPlain),
35   STARTTLS, VRFY, EXPN QUIT
36 
37  Supported authentication methods:
38   PLAIN
39  +/
40 class SmtpClient {
41 protected:
42 	char[1024] _recvbuf;
43 
44 	bool _secure;
45 	bool _authenticated;
46 
47 	InternetAddress server;
48 	Socket transport;
49 
50 	// SSL Enabled
51 	version (ssl) {
52 	SocketSSL secureTransport;
53 	}
54 
55 	/++
56 	 Convenience method to send whole buffer of data into socket.
57 	 +/
58 	bool sendData(in char[] data) {
59 		// SSL Enabled
60 		version(ssl) {
61 			if (!this._secure) {
62 				ptrdiff_t sent = 0;
63 				while (sent < data.length) {
64 					sent += this.transport.send(data);
65 				}
66 				return true;
67 			} else {
68 				return secureTransport.write(data) ? true : false;
69 			}
70 		// SSL Disabled
71 		} else {
72 			ptrdiff_t sent = 0;
73 			while (sent < data.length) {
74 				sent += this.transport.send(data);
75 			}
76 			return true;
77 		}
78 	}
79 
80 	/++
81 	 Convenience method to receive data from socket as a string.
82 	 +/
83 	string receiveData() {
84 		// SSL Enabled
85 		version(ssl) {
86 			if (!this._secure) {
87 				ptrdiff_t bytesReceived = this.transport.receive(_recvbuf);
88 				return to!string(_recvbuf[0 .. bytesReceived]);
89 			} else {
90 				return secureTransport.read();
91 			}
92 		// SSL Disabled
93 		} else {
94 			ptrdiff_t bytesReceived = this.transport.receive(_recvbuf);
95 			return to!string(_recvbuf[0 .. bytesReceived]);
96 		}
97 	}
98 
99 	/++
100 	 Parses server reply and converts it to 'SmtpReply' structure.
101 	 +/
102 	SmtpReply parseReply(string rawReply) {
103 		auto reply = SmtpReply(
104 			true,
105 			to!uint(rawReply[0 .. 3]),
106 			(rawReply[3 .. $]).idup
107 		);
108 		if (reply.code >= 400) {
109 			reply.success = false;
110 		}
111 		return reply;
112 	}
113 
114 	/++
115 	 Implementation of request/response pattern for easifying
116 	 communication with SMTP server.
117 	 +/
118 	string getResponse(string command, string suffix="\r\n") {
119 		sendData(command ~ suffix);
120 		auto response = "";
121 		while (!response.endsWith("\n")) {
122 			auto received = receiveData();
123 			response ~= received;
124 			if (received == "") break;
125 		}
126 		return response;
127 	}
128 
129 public:
130 	this(string host, ushort port = 25) {
131 		auto addr = new InternetHost;
132 		if (addr.getHostByName(host)) {
133 			server = new InternetAddress(addr.addrList[0], port);
134 		} else {
135 		}
136 		transport = new TcpSocket(AddressFamily.INET);
137 	}
138 
139 	@property bool secure() const { return _secure; }
140 	@property Address address() { return this.server; }
141 	@property authenticated() const { return _authenticated; }
142 
143 	/++
144 	 Performs socket connection establishment.
145 	 connect is the first method to be called after SmtpClient instantiation.
146 	 +/
147 	SmtpReply connect() {
148 
149 		if (this.server is null)		
150 			return SmtpReply(false, 0, "");
151 	
152 		try {
153 			this.transport.connect(this.server);
154 		} catch (SocketOSException) {
155 			return SmtpReply(false, 0, "");
156 		}
157 		return parseReply(receiveData());
158 	}
159 
160 	/++
161 	 Send command indicating that TLS encrypting of socket data stream has started.
162 	 +/
163 	final SmtpReply starttls(EncryptionMethod enctype = EncryptionMethod.SSLv3, bool verifyCertificate = false) {
164 		version(ssl) {
165 		auto response = parseReply(getResponse("STARTTLS"));
166 		if (response.success) {
167 			secureTransport = new SocketSSL(transport, enctype);
168 			_secure = verifyCertificate ? secureTransport.ready && secureTransport.certificateIsVerified : secureTransport.ready;
169 		}
170 		return response;
171 		} else {
172 		return SmtpReply(false, 0, "");
173 		}
174 	}
175 
176 	/++
177 	 A `no operation` message. essage that does not invoke any specific routine
178 	 on server, but still the reply is received.
179 	 +/
180 	final SmtpReply noop() {
181 		return parseReply(getResponse("NOOP"));
182 	}
183 
184 	/++
185 	 Initial message to send after connection.
186 	 Nevertheless it is recommended to use `ehlo()` instead of this method
187 	 in order to get more information about SMTP server configuration.
188 	 +/
189 	final SmtpReply helo() {
190 		return parseReply(getResponse("HELO " ~ transport.hostName));
191 	}
192 
193 	/++
194 	 Initial message to send after connection.
195 	 Retrieves information about SMTP server configuration
196 	 +/
197 	final SmtpReply ehlo() {
198 		return parseReply(getResponse("EHLO " ~ transport.hostName));
199 	}
200 
201 	/++
202 	 Perform authentication (according to RFC 4954)
203 	 +/
204 	final SmtpReply auth(in SmtpAuthType authType) {
205 		return parseReply(getResponse("AUTH " ~ to!string(authType)));
206 	}
207 
208 	/++
209 	 Send base64-encoded authentication data according to RFC 2245.
210 	 Need to be performed after `data` method call;
211 	 +/
212 	final SmtpReply authPlain(string login, string password) {
213 		string data = login ~ "\0" ~ login ~ "\0" ~ password;
214 		const(char)[] encoded = Base64.encode(cast(ubyte[])data);
215 		return parseReply(getResponse(to!string(encoded)));
216 	}
217 
218 	/++
219 	 Sends base64-encoded login
220 	 +/
221 	final SmtpReply authLoginUsername(string login) {
222 		const(char)[] encoded = Base64.encode(cast(ubyte[]) login);
223 		return parseReply(getResponse(to!string(encoded)));
224 	}
225 
226 	/++
227 	 Sends base64-encode password
228 	 +/
229 	final SmtpReply authLoginPassword(string password) {
230 		const(char)[] encoded = Base64.encode(cast(ubyte[]) password);
231 		return parseReply(getResponse(to!string(encoded)));
232 	}
233 
234 	/++
235 	 Low-level method to initiate process of sending mail.
236 	 This can be called either after connect or after helo/ehlo methods call.
237 	 +/
238 	final SmtpReply mail(string address) {
239 		return parseReply(getResponse("MAIL FROM:<" ~ address ~ ">"));
240 	}
241 
242 	/++
243 	 Low-level method to specify recipients of the mail. Must be called
244 	 after
245 	 +/
246 	final SmtpReply rcpt(string to) {
247 		return parseReply(getResponse("RCPT TO:<" ~ to ~ ">"));
248 	}
249 
250 	/++
251 	 Low-level method to initiate sending of the message body.
252 	 Must be called after rcpt method call.
253 	 +/
254 	final SmtpReply data() {
255 		return parseReply(getResponse("DATA"));
256 	}
257 
258 	/++
259 	 Sends the body of message to server. Must be called after `data` method.
260 	 Also dataBody sends needed suffix to signal about the end of the message body.
261 	 +/
262 	final SmtpReply dataBody(string message) {
263 		return parseReply(getResponse(message, "\r\n.\r\n"));
264 	}
265 
266 	/++
267 	 This method cancels mail transmission if mail session is in progress
268 	 (after `mail` method was called).
269 	 +/
270 	final SmtpReply rset() {
271 		return parseReply(getResponse("RSET"));
272 	}
273 
274 	/++
275 	 This method asks server to verify if the user's mailbox exists on server.
276 	 You can pass [username] or <[e-mail]> as an argument. The result is a reply
277 	 that contains e-mail of the user, or an error code.
278 
279 	 IMPORTANT NOTE: most of servers forbid using of VRFY considering it to
280 	 be a security hole.
281 	 +/
282 	final SmtpReply vrfy(string username) {
283 		return parseReply(getResponse("VRFY " ~ username));
284 	}
285 
286 	/++
287 	 EXPN expands mailing list on the server. If mailing list is not available
288 	 for you, you receive appropriate error reply, else you receive a list of
289 	 mailiing list subscribers.
290 	 +/
291 	final SmtpReply expn(string mailinglist) {
292 		return parseReply(getResponse("EXPN " ~ mailinglist));
293 	}
294 
295 	/++
296 	 Performs disconnection from server. In one session several mails can be sent,
297 	 and it is recommended to do so. quit forces server to close connection with
298 	 client.
299 	 +/
300 	final SmtpReply quit() {
301 		return parseReply(getResponse("QUIT"));
302 	}
303 
304 	/++
305 	 Performs clean disconnection from server.
306 	 It is recommended to use disconnect after quit method which signals
307 	 SMTP server about end of the session.
308 	 +/
309 	void disconnect() {
310 		this.transport.shutdown(SocketShutdown.BOTH);
311 		this.transport.close();
312 	}
313 
314 }