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