1 module smtp.mailsender;
2 
3 import core.exception;
4 import core.sync.mutex;
5 
6 import std.algorithm;
7 import std.conv;
8 import std.stdio;
9 import std.string;
10 import std.traits;
11 
12 import smtp.client;
13 import smtp.message;
14 import smtp.reply;
15 
16 version(ssl) {
17 import smtp.ssl;
18 }
19 
20 /++
21  High-level implementation of SMTP client.
22  +/
23 class MailSender {
24 
25 private:
26 	SmtpClient _smtp_client;
27 
28 	version(ssl) {
29 	EncryptionMethod _encType;
30 	}
31 	bool _server_supports_pipelining = false;
32 	bool _server_supports_vrfy = false;
33 	bool _server_supports_etrn = false;
34 	bool _server_supports_enhanced_status_codes = false;
35 	bool _server_supports_dsn = false;
36 	bool _server_supports_8bitmime = false;
37 	bool _server_supports_binarymime = false;
38 	bool _server_supports_chunking = false;
39 	bool _server_supports_encryption = false;
40 
41 	uint _max_message_size = 0;
42 
43 	Mutex _transmission_lock;
44 
45 	SmtpReply connect_impl() {
46 		_transmission_lock.lock();
47 		scope(exit) _transmission_lock.unlock();
48 		auto reply = _smtp_client.connect();
49 		return reply;
50 	}
51 
52 	SmtpReply get_server_capabilities() {
53 		_transmission_lock.lock();
54 		scope(exit) _transmission_lock.unlock();
55 		// Trying to get what possibilities server supports
56 		auto reply = _smtp_client.ehlo();
57 		foreach(line; split(strip(reply.message), "\r\n")[1 .. $ - 1]) {
58 			auto extension = line[4 .. $];
59 			switch(extension) {
60 			case "STARTTLS":
61 				_server_supports_encryption = true;
62 				break;
63 			case "PIPELINING":
64 				_server_supports_pipelining = true;
65 				break;
66 			case "VRFY":
67 				_server_supports_vrfy = true;
68 				break;
69 			case "ETRN":
70 				_server_supports_etrn = true;
71 				break;
72 			case "ENHANCEDSTATUSCODES":
73 				_server_supports_enhanced_status_codes = true;
74 				break;
75 			case "DSN":
76 				_server_supports_dsn = true;
77 				break;
78 			case "8BITMIME":
79 				_server_supports_8bitmime = true;
80 				break;
81 			case "BINARYMIME":
82 				_server_supports_binarymime = true;
83 				break;
84 			default:
85 			}
86 		}
87 
88 		foreach(line; split(strip(reply.message), "\r\n")[1 .. $]) {
89 			auto option = line[4 .. $];
90 			if (option.startsWith("SIZE")) {
91   			try	{
92 					_max_message_size = to!int(line[9 .. $]);
93 				} catch (RangeError) {
94 				}
95 				continue;
96 			} else if (option.startsWith("CHUNKING")) {
97 				_server_supports_chunking = true;
98 				continue;
99 			}
100 		}
101 		return reply;
102 	}
103 
104 public:
105 version(ssl) {
106 	/++
107 	 SSL-enabled constructor
108 	 +/
109 	this(string host, ushort port, EncryptionMethod encType = EncryptionMethod.None) {
110 		_smtp_client = new SmtpClient(host, port);
111 		_encType = encType;
112 		_transmission_lock = new Mutex();
113 	}
114 } else {
115 	/++
116 	 No-SSL constructor
117 	 +/
118 	this(string host, ushort port) {
119 		_smtp_client = new SmtpClient(host, port);
120 		_transmission_lock = new Mutex();
121 	}
122 }
123 
124 	/++
125 	 Server limits
126 	 +/
127 	uint maxMessageSize() const { return _max_message_size; }
128 
129 	/++
130 	 Server-supported extensions
131 	 +/
132 	bool extensionPipelining() const { return _server_supports_pipelining; }
133 	bool extensionVrfy() const { return _server_supports_vrfy; }
134 	bool extensionEtrn() const { return _server_supports_etrn; }
135 	bool extensionEnhancedStatusCodes() const { return _server_supports_enhanced_status_codes; }
136 	bool extensionDsn() const { return _server_supports_dsn; }
137 	bool extension8bitMime() const { return _server_supports_8bitmime; }
138 	bool extensionBinaryMime() const { return _server_supports_binarymime; }
139 	bool extensionChunking() const { return _server_supports_chunking; }
140 	bool extensionTls() const { return _server_supports_encryption; }
141 
142 version(ssl){
143 	/++
144 	 Connecting to SMTP server and also trying to get server possibiities
145 	 in order to expose it via public API.
146 	 +/
147 	SmtpReply connect() {
148 		auto reply = connect_impl();
149 		if(!reply.success) return reply;
150 
151 		reply = get_server_capabilities();
152 		if(!reply.success) return reply;
153 
154 		_transmission_lock.lock();
155 		_smtp_client.starttls();
156 		_transmission_lock.unlock();
157 
158 		reply = get_server_capabilities();
159 		return reply;
160 	}
161 } else {
162 	/++
163 	 Connecting to SMTP server and also trying to get server possibiities
164 	 in order to expose it via public API.
165 	 +/
166 	SmtpReply connect() {
167 		auto reply = connect_impl();
168 		if(!reply.success) return reply;
169 
170 		return get_server_capabilities();
171 	}
172 }
173 
174 	/++
175 	 Perfrom authentication process in one method (high-level) instead
176 	 of sending AUTH and auth data in several messages.
177 
178 	 Auth schemes accoring to type:
179 	  * PLAIN:
180 	    | AUTH->, <-STATUS, [encoded login/password]->, <-STATUS
181 	  * LOGIN:
182 	    | AUTH->, <-STATUS, [encoded login]->, <-STATUS, [encoded password]->, <-STATUS
183 	 +/
184 	 SmtpReply authenticate(A...)(in SmtpAuthType authType, A params) {
185 	 	_transmission_lock.lock();
186 	 	SmtpReply result;
187 	 	final switch (authType) {
188 	 	case SmtpAuthType.PLAIN:
189 	 		static assert((params.length == 2) && is(A[0] == string) && is(A[1] == string));
190 			auto reply = _smtp_client.auth(authType);
191 			result = reply.success ? _smtp_client.authPlain(params[0], params[1]) : reply;
192 			break;
193 	 	case SmtpAuthType.LOGIN:
194 	 		static assert((params.length == 2) && is(A[0] == string) && is(A[1] == string));
195 			auto reply = _smtp_client.auth(authType);
196 			if (reply.success) {
197 				reply = _smtp_client.authLoginUsername(params[0]);
198 				result = reply.success ? _smtp_client.authLoginPassword(params[1]) : reply;
199 			} else {
200 				result = reply;
201 			}
202 			break;
203 	 	}
204 		_transmission_lock.unlock();
205 		return result;
206 	 }
207 
208 	/++
209 	 High-level method for sending messages.
210 
211 	 Accepts SmtpMessage instance and returns true
212 	 if message was sent successfully or false otherwise.
213 
214 	 This method is recommended in order to simplify the whole workflow
215 	 with the `smtp` library.
216 
217 	 send method basically implements [mail -> rcpt ... rcpt -> data -> dataBody]
218 	 method calls chain.
219 	 +/
220 	SmtpReply send(in SmtpMessage mail) {
221 		_transmission_lock.lock();
222 		auto reply = _smtp_client.mail(mail.sender.address);
223 		if (!reply.success) return reply;
224 		foreach (i, recipient; mail.recipients) {
225 			reply = _smtp_client.rcpt(recipient.address);
226 			if (!reply.success) {
227 				_smtp_client.rset();
228 				_transmission_lock.unlock();
229 				return reply;
230 			}
231 		}
232 		reply = _smtp_client.data();
233 		if (!reply.success) {
234 			_smtp_client.rset();
235 			_transmission_lock.unlock();
236 			return reply;
237 		}
238 		reply = _smtp_client.dataBody(mail.toString());
239 		if (!reply.success) {
240 			_smtp_client.rset();
241 		}
242 		_transmission_lock.unlock();
243 		return reply;
244 	}
245 
246 	/++
247 	 High-level method for sending 'quit' message to SMTP server.
248 
249 	 This method must be performed in order to notify server that
250 	 the client is going to finish its work with it.
251 	+/
252 	SmtpReply quit() {
253 		_transmission_lock.lock();
254 		auto reply = _smtp_client.quit();
255 		_transmission_lock.unlock();
256 		return reply;
257 	}
258 
259 	/++
260 	 Perform clean shutdown for allocated resources.
261 	 +/
262 	~this() {
263 		_smtp_client.disconnect();
264 	}
265 }