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 }