1
/* Copyright 2016 Software Freedom Conservancy Inc.
3
* This software is licensed under the GNU Lesser General Public License
4
* (version 2.1 or later). See the COPYING file in this distribution.
7
public class Geary.Smtp.ClientConnection {
9
public const uint DEFAULT_TIMEOUT_SEC = 20;
11
public Geary.Smtp.Capabilities? capabilities { get; private set; default = null; }
13
private Geary.Endpoint endpoint;
14
private IOStream? cx = null;
15
private SocketConnection? socket_cx = null;
16
private DataInputStream? dins = null;
17
private DataOutputStream douts = null;
19
public ClientConnection(Geary.Endpoint endpoint) {
20
this.endpoint = endpoint;
23
public bool is_connected() {
27
public async Greeting? connect_async(Cancellable? cancellable = null) throws Error {
29
debug("Already connected to %s", to_string());
34
cx = socket_cx = yield endpoint.connect_async(cancellable);
37
// read and deserialize the greeting
38
Greeting greeting = new Greeting(yield recv_response_lines_async(cancellable));
39
Logging.debug(Logging.Flag.NETWORK, "[%s] SMTP Greeting: %s", to_string(), greeting.to_string());
44
public async bool disconnect_async(Cancellable? cancellable = null) throws Error {
48
Error? disconnect_error = null;
50
yield cx.close_async(Priority.DEFAULT, cancellable);
52
disconnect_error = err;
57
if (disconnect_error != null)
58
throw disconnect_error;
64
* Returns the final Response of the challenge-response.
66
public async Response authenticate_async(Authenticator authenticator, Cancellable? cancellable = null)
70
Response response = yield transaction_async(authenticator.initiate(), cancellable);
72
Logging.debug(Logging.Flag.NETWORK, "[%s] Initiated SMTP %s authentication", to_string(),
73
authenticator.to_string());
75
// Possible for initiate() Request to:
76
// (a) immediately generate success (due to valid authentication being passed in Request);
77
// (b) immediately fails;
78
// or (c) result in response asking for more.
80
// Only (c) keeps the challenge-response alive. Other possibilities means the process has
83
while (response.code.is_success_intermediate()) {
84
Memory.Buffer? data = authenticator.challenge(step++, response);
85
if (data == null || data.size == 0)
86
data = new Memory.StringBuffer(DataFormat.CANCEL_AUTHENTICATION);
88
Logging.debug(Logging.Flag.NETWORK, "[%s] SMTP AUTH Challenge recvd", to_string());
90
yield Stream.write_all_async(douts, data, cancellable);
91
douts.put_string(DataFormat.LINE_TERMINATOR);
92
yield douts.flush_async(Priority.DEFAULT, cancellable);
94
response = yield recv_response_async(cancellable);
101
* Sends a block of data (mail message) by first issuing the DATA command and transmitting
102
* the block if the appropriate response is sent.
104
* Dot-stuffing is performed on the data if !already_dotstuffed. See
105
* [[http://tools.ietf.org/html/rfc2821#section-4.5.2]]
107
* Returns the final Response of the transaction. If the ResponseCode is not a successful
108
* completion, the message should not be considered sent.
110
public async Response send_data_async(Memory.Buffer data, bool already_dotstuffed,
111
Cancellable? cancellable = null) throws Error {
114
// In the case of DATA, want to receive an intermediate response code, specifically 354
115
Response response = yield transaction_async(new Request(Command.DATA), cancellable);
116
if (!response.code.is_start_data())
119
Logging.debug(Logging.Flag.NETWORK, "[%s] SMTP Data: <%z>", to_string(), data.size);
121
if (!already_dotstuffed) {
122
// By using DataStreamNewlineType.ANY, we're assured to get each line and convert to
123
// a proper line terminator for SMTP
124
DataInputStream dins = new DataInputStream(data.get_input_stream());
125
dins.set_newline_type(DataStreamNewlineType.ANY);
127
// Read each line and dot-stuff if necessary
130
string? line = yield dins.read_line_async(Priority.DEFAULT, cancellable, out length);
136
yield Stream.write_string_async(douts, ".", cancellable);
138
yield Stream.write_string_async(douts, line, cancellable);
139
yield Stream.write_string_async(douts, DataFormat.LINE_TERMINATOR, cancellable);
142
// ready to go, send and commit
143
yield Stream.write_all_async(douts, data, cancellable);
146
// terminate buffer and flush to server
147
yield Stream.write_string_async(douts, DataFormat.DATA_TERMINATOR, cancellable);
148
yield douts.flush_async(Priority.DEFAULT, cancellable);
150
return yield recv_response_async(cancellable);
153
public async void send_request_async(Request request, Cancellable? cancellable = null) throws Error {
156
Logging.debug(Logging.Flag.NETWORK, "[%s] SMTP Request: %s", to_string(), request.to_string());
158
douts.put_string(request.serialize());
159
douts.put_string(DataFormat.LINE_TERMINATOR);
160
yield douts.flush_async(Priority.DEFAULT, cancellable);
163
private async Gee.List<ResponseLine> recv_response_lines_async(Cancellable? cancellable) throws Error {
166
Gee.List<ResponseLine> lines = new Gee.ArrayList<ResponseLine>();
168
ResponseLine line = ResponseLine.deserialize(yield read_line_async(cancellable));
175
// lines should never be empty; if it is, then somebody didn't throw an exception
176
assert(lines.size > 0);
181
public async Response recv_response_async(Cancellable? cancellable = null) throws Error {
182
Response response = new Response(yield recv_response_lines_async(cancellable));
184
Logging.debug(Logging.Flag.NETWORK, "[%s] SMTP Response: %s", to_string(), response.to_string());
190
* Sends the appropriate HELO/EHLO command and returns the response of the one that worked.
191
* Also saves the server's capabilities in the capabilities property (overwriting any that may
192
* already be present).
194
public async Response say_hello_async(Cancellable? cancellable) throws Error {
195
// get local address as FQDN to greet server ... note that this merely returns the DHCP address
196
// for machines behind a NAT
197
InetAddress local_addr = ((InetSocketAddress) socket_cx.get_local_address()).get_address();
199
// only attempt to produce a FQDN if not a local address and use the local address if
202
if (!local_addr.is_link_local && !local_addr.is_loopback && !local_addr.is_site_local) {
204
fqdn = yield Resolver.get_default().lookup_by_address_async(local_addr, cancellable);
205
} catch (Error err) {
206
debug("[%s] Unable to lookup local address for %s: %s", to_string(),
207
local_addr.to_string(), err.message);
211
// try EHLO first, then fall back on HELO
212
EhloRequest ehlo = !String.is_empty(fqdn) ? new EhloRequest(fqdn) : new EhloRequest.for_local_address(local_addr);
213
Response response = yield transaction_async(ehlo, cancellable);
214
if (response.code.is_success_completed()) {
215
// save list of caps returned in EHLO command
216
capabilities = new Geary.Smtp.Capabilities();
217
capabilities.add_ehlo_response(response);
219
string first_response = response.to_string().strip();
220
HeloRequest helo = !String.is_empty(fqdn) ? new HeloRequest(fqdn) : new HeloRequest.for_local_address(local_addr);
221
response = yield transaction_async(helo, cancellable);
222
if (!response.code.is_success_completed()) {
223
throw new SmtpError.SERVER_ERROR("Refused service: \"%s\" and \"%s\"", first_response,
224
response.to_string().strip());
232
* Sends the appropriate hello command to the server (EHLO / HELO) and establishes whatever
233
* additional connection features are available (STARTTLS, compression). For general-purpose
234
* use, this is the preferred method for establishing a session with a server, as it will do
235
* whatever is necessary to ensure quality-of-service and security.
237
* Note that this does *not* connect to the server; connect_async() should be used before
238
* calling this method.
240
* Returns the Response of the final hello command (there may be more than one).
242
public async Response establish_connection_async(Cancellable? cancellable = null) throws Error {
245
// issue first HELO/EHLO, which will generate a set of capabiltiies
246
Smtp.Response response = yield say_hello_async(cancellable);
248
// STARTTLS, if required
249
if (endpoint.tls_method == TlsNegotiationMethod.START_TLS) {
250
if (!capabilities.has_capability(Capabilities.STARTTLS)) {
251
throw new SmtpError.NOT_SUPPORTED(
252
"STARTTLS not available for %s", endpoint.to_string()
256
Response starttls_response = yield transaction_async(new Request(Command.STARTTLS));
257
if (!starttls_response.code.is_starttls_ready())
258
throw new SmtpError.STARTTLS_FAILED("STARTTLS failed: %s", response.to_string());
260
TlsClientConnection tls_cx = yield endpoint.starttls_handshake_async(cx, cancellable);
262
set_data_streams(tls_cx);
264
// Now that we are on an encrypted line we need to say hello again in order to get the
265
// updated capabilities.
266
response = yield say_hello_async(cancellable);
272
public async Response quit_async(Cancellable? cancellable = null) throws Error {
274
return yield transaction_async(new Request(Command.QUIT), cancellable);
277
public async Response transaction_async(Request request, Cancellable? cancellable = null)
279
yield send_request_async(request, cancellable);
281
return yield recv_response_async(cancellable);
284
private async string read_line_async(Cancellable? cancellable) throws Error {
286
string? line = yield dins.read_line_async(Priority.DEFAULT, cancellable, out length);
288
if (String.is_empty(line))
289
throw new IOError.CLOSED("End of stream detected on %s", to_string());
294
private void check_connected() throws Error {
296
throw new SmtpError.NOT_CONNECTED("Not connected to %s", to_string());
299
public string to_string() {
300
return endpoint.to_string();
303
private void set_data_streams(IOStream stream) {
304
dins = new DataInputStream(stream.input_stream);
305
dins.set_newline_type(DataFormat.LINE_TERMINATOR_TYPE);
306
dins.set_close_base_stream(false);
307
douts = new DataOutputStream(stream.output_stream);
308
douts.set_close_base_stream(false);