~hkdb/geary/disco

« back to all changes in this revision

Viewing changes to src/engine/smtp/smtp-client-connection.vala

  • Committer: hkdb
  • Date: 2019-09-26 19:40:48 UTC
  • Revision ID: hkdb@3df.io-20190926194048-n0vggm3yfo8p1ubr
Tags: upstream-3.32.2-disco
ImportĀ upstreamĀ versionĀ 3.32.2-disco

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/* Copyright 2016 Software Freedom Conservancy Inc.
 
2
 *
 
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.
 
5
 */
 
6
 
 
7
public class Geary.Smtp.ClientConnection {
 
8
 
 
9
    public const uint DEFAULT_TIMEOUT_SEC = 20;
 
10
 
 
11
    public Geary.Smtp.Capabilities? capabilities { get; private set; default = null; }
 
12
 
 
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;
 
18
 
 
19
    public ClientConnection(Geary.Endpoint endpoint) {
 
20
        this.endpoint = endpoint;
 
21
    }
 
22
 
 
23
    public bool is_connected() {
 
24
        return (cx != null);
 
25
    }
 
26
 
 
27
    public async Greeting? connect_async(Cancellable? cancellable = null) throws Error {
 
28
        if (cx != null) {
 
29
            debug("Already connected to %s", to_string());
 
30
 
 
31
            return null;
 
32
        }
 
33
 
 
34
        cx = socket_cx = yield endpoint.connect_async(cancellable);
 
35
        set_data_streams(cx);
 
36
 
 
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());
 
40
 
 
41
        return greeting;
 
42
    }
 
43
 
 
44
    public async bool disconnect_async(Cancellable? cancellable = null) throws Error {
 
45
        if (cx == null)
 
46
            return false;
 
47
 
 
48
        Error? disconnect_error = null;
 
49
        try {
 
50
            yield cx.close_async(Priority.DEFAULT, cancellable);
 
51
        } catch (Error err) {
 
52
            disconnect_error = err;
 
53
        }
 
54
 
 
55
        cx = null;
 
56
 
 
57
        if (disconnect_error != null)
 
58
            throw disconnect_error;
 
59
 
 
60
        return true;
 
61
    }
 
62
 
 
63
    /**
 
64
     * Returns the final Response of the challenge-response.
 
65
     */
 
66
    public async Response authenticate_async(Authenticator authenticator, Cancellable? cancellable = null)
 
67
        throws Error {
 
68
        check_connected();
 
69
 
 
70
        Response response = yield transaction_async(authenticator.initiate(), cancellable);
 
71
 
 
72
        Logging.debug(Logging.Flag.NETWORK, "[%s] Initiated SMTP %s authentication", to_string(),
 
73
            authenticator.to_string());
 
74
 
 
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.
 
79
        //
 
80
        // Only (c) keeps the challenge-response alive.  Other possibilities means the process has
 
81
        // completed.
 
82
        int step = 0;
 
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);
 
87
 
 
88
            Logging.debug(Logging.Flag.NETWORK, "[%s] SMTP AUTH Challenge recvd", to_string());
 
89
 
 
90
            yield Stream.write_all_async(douts, data, cancellable);
 
91
            douts.put_string(DataFormat.LINE_TERMINATOR);
 
92
            yield douts.flush_async(Priority.DEFAULT, cancellable);
 
93
 
 
94
            response = yield recv_response_async(cancellable);
 
95
        }
 
96
 
 
97
        return response;
 
98
    }
 
99
 
 
100
    /**
 
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.
 
103
     *
 
104
     * Dot-stuffing is performed on the data if !already_dotstuffed.  See
 
105
     * [[http://tools.ietf.org/html/rfc2821#section-4.5.2]]
 
106
     *
 
107
     * Returns the final Response of the transaction.  If the ResponseCode is not a successful
 
108
     * completion, the message should not be considered sent.
 
109
     */
 
110
    public async Response send_data_async(Memory.Buffer data, bool already_dotstuffed,
 
111
        Cancellable? cancellable = null) throws Error {
 
112
        check_connected();
 
113
 
 
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())
 
117
            return response;
 
118
 
 
119
        Logging.debug(Logging.Flag.NETWORK, "[%s] SMTP Data: <%z>", to_string(), data.size);
 
120
 
 
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);
 
126
 
 
127
            // Read each line and dot-stuff if necessary
 
128
            for (;;) {
 
129
                size_t length;
 
130
                string? line = yield dins.read_line_async(Priority.DEFAULT, cancellable, out length);
 
131
                if (line == null)
 
132
                    break;
 
133
 
 
134
                // stuffing
 
135
                if (line[0] == '.')
 
136
                    yield Stream.write_string_async(douts, ".", cancellable);
 
137
 
 
138
                yield Stream.write_string_async(douts, line, cancellable);
 
139
                yield Stream.write_string_async(douts, DataFormat.LINE_TERMINATOR, cancellable);
 
140
            }
 
141
        } else {
 
142
            // ready to go, send and commit
 
143
            yield Stream.write_all_async(douts, data, cancellable);
 
144
        }
 
145
 
 
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);
 
149
 
 
150
        return yield recv_response_async(cancellable);
 
151
    }
 
152
 
 
153
    public async void send_request_async(Request request, Cancellable? cancellable = null) throws Error {
 
154
        check_connected();
 
155
 
 
156
        Logging.debug(Logging.Flag.NETWORK, "[%s] SMTP Request: %s", to_string(), request.to_string());
 
157
 
 
158
        douts.put_string(request.serialize());
 
159
        douts.put_string(DataFormat.LINE_TERMINATOR);
 
160
        yield douts.flush_async(Priority.DEFAULT, cancellable);
 
161
    }
 
162
 
 
163
    private async Gee.List<ResponseLine> recv_response_lines_async(Cancellable? cancellable) throws Error {
 
164
        check_connected();
 
165
 
 
166
        Gee.List<ResponseLine> lines = new Gee.ArrayList<ResponseLine>();
 
167
        for (;;) {
 
168
            ResponseLine line = ResponseLine.deserialize(yield read_line_async(cancellable));
 
169
            lines.add(line);
 
170
 
 
171
            if (!line.continued)
 
172
                break;
 
173
        }
 
174
 
 
175
        // lines should never be empty; if it is, then somebody didn't throw an exception
 
176
        assert(lines.size > 0);
 
177
 
 
178
        return lines;
 
179
    }
 
180
 
 
181
    public async Response recv_response_async(Cancellable? cancellable = null) throws Error {
 
182
        Response response = new Response(yield recv_response_lines_async(cancellable));
 
183
 
 
184
        Logging.debug(Logging.Flag.NETWORK, "[%s] SMTP Response: %s", to_string(), response.to_string());
 
185
 
 
186
        return response;
 
187
    }
 
188
 
 
189
    /**
 
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).
 
193
     */
 
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();
 
198
 
 
199
        // only attempt to produce a FQDN if not a local address and use the local address if
 
200
        // unavailable
 
201
        string? fqdn = null;
 
202
        if (!local_addr.is_link_local && !local_addr.is_loopback && !local_addr.is_site_local) {
 
203
            try {
 
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);
 
208
            }
 
209
        }
 
210
 
 
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);
 
218
        } else {
 
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());
 
225
            }
 
226
        }
 
227
 
 
228
        return response;
 
229
    }
 
230
 
 
231
    /**
 
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.
 
236
     *
 
237
     * Note that this does *not* connect to the server; connect_async() should be used before
 
238
     * calling this method.
 
239
     *
 
240
     * Returns the Response of the final hello command (there may be more than one).
 
241
     */
 
242
    public async Response establish_connection_async(Cancellable? cancellable = null) throws Error {
 
243
        check_connected();
 
244
 
 
245
        // issue first HELO/EHLO, which will generate a set of capabiltiies
 
246
        Smtp.Response response = yield say_hello_async(cancellable);
 
247
 
 
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()
 
253
                );
 
254
            }
 
255
 
 
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());
 
259
 
 
260
            TlsClientConnection tls_cx = yield endpoint.starttls_handshake_async(cx, cancellable);
 
261
            cx = tls_cx;
 
262
            set_data_streams(tls_cx);
 
263
 
 
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);
 
267
        }
 
268
 
 
269
        return response;
 
270
    }
 
271
 
 
272
    public async Response quit_async(Cancellable? cancellable = null) throws Error {
 
273
        capabilities = null;
 
274
        return yield transaction_async(new Request(Command.QUIT), cancellable);
 
275
    }
 
276
 
 
277
    public async Response transaction_async(Request request, Cancellable? cancellable = null)
 
278
        throws Error {
 
279
        yield send_request_async(request, cancellable);
 
280
 
 
281
        return yield recv_response_async(cancellable);
 
282
    }
 
283
 
 
284
    private async string read_line_async(Cancellable? cancellable) throws Error {
 
285
        size_t length;
 
286
        string? line = yield dins.read_line_async(Priority.DEFAULT, cancellable, out length);
 
287
 
 
288
        if (String.is_empty(line))
 
289
            throw new IOError.CLOSED("End of stream detected on %s", to_string());
 
290
 
 
291
        return line;
 
292
    }
 
293
 
 
294
    private void check_connected() throws Error {
 
295
        if (cx == null)
 
296
            throw new SmtpError.NOT_CONNECTED("Not connected to %s", to_string());
 
297
    }
 
298
 
 
299
    public string to_string() {
 
300
        return endpoint.to_string();
 
301
    }
 
302
 
 
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);
 
309
    }
 
310
}
 
311