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 }