Add support for (X)OAuth2 IMAP authentication.
This commit is contained in:
parent
ada7f3fdbb
commit
865f23beba
8 changed files with 237 additions and 10 deletions
|
|
@ -294,6 +294,7 @@ src/engine/imap/message/imap-tag.vala
|
|||
src/engine/imap/message/imap-uid.vala
|
||||
src/engine/imap/message/imap-uid-validity.vala
|
||||
src/engine/imap/parameter/imap-atom-parameter.vala
|
||||
src/engine/imap/parameter/imap-continuation-parameter.vala
|
||||
src/engine/imap/parameter/imap-list-parameter.vala
|
||||
src/engine/imap/parameter/imap-literal-parameter.vala
|
||||
src/engine/imap/parameter/imap-nil-parameter.vala
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ engine/imap/api/imap-folder-root.vala
|
|||
engine/imap/api/imap-folder-session.vala
|
||||
engine/imap/api/imap-session-object.vala
|
||||
engine/imap/command/imap-append-command.vala
|
||||
engine/imap/command/imap-authenticate-command.vala
|
||||
engine/imap/command/imap-capability-command.vala
|
||||
engine/imap/command/imap-close-command.vala
|
||||
engine/imap/command/imap-command.vala
|
||||
|
|
@ -140,6 +141,7 @@ engine/imap/message/imap-tag.vala
|
|||
engine/imap/message/imap-uid.vala
|
||||
engine/imap/message/imap-uid-validity.vala
|
||||
engine/imap/parameter/imap-atom-parameter.vala
|
||||
engine/imap/parameter/imap-continuation-parameter.vala
|
||||
engine/imap/parameter/imap-list-parameter.vala
|
||||
engine/imap/parameter/imap-literal-parameter.vala
|
||||
engine/imap/parameter/imap-nil-parameter.vala
|
||||
|
|
|
|||
58
src/engine/imap/command/imap-authenticate-command.vala
Normal file
58
src/engine/imap/command/imap-authenticate-command.vala
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright 2018 Michael Gratton <mike@vee.net>
|
||||
*
|
||||
* This software is licensed under the GNU Lesser General Public License
|
||||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
/**
|
||||
* An IMAP AUTHENTICATE command.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-6.2.2]]
|
||||
*/
|
||||
public class Geary.Imap.AuthenticateCommand : Command {
|
||||
|
||||
|
||||
public const string NAME = "authenticate";
|
||||
|
||||
private const string OAUTH2_METHOD = "xoauth2";
|
||||
private const string OAUTH2_RESP = "user=%s\001auth=Bearer %s\001\001";
|
||||
|
||||
|
||||
public string method { get; private set; }
|
||||
|
||||
|
||||
private AuthenticateCommand(string method, string data) {
|
||||
base(NAME, { method, data });
|
||||
this.method = method;
|
||||
}
|
||||
|
||||
public AuthenticateCommand.oauth2(string user, string token) {
|
||||
string encoded_token = Base64.encode(
|
||||
OAUTH2_RESP.printf(user, token).data
|
||||
);
|
||||
this(OAUTH2_METHOD, encoded_token);
|
||||
}
|
||||
|
||||
public ContinuationParameter
|
||||
continuation_requested(ContinuationResponse response)
|
||||
throws ImapError {
|
||||
if (this.method != AuthenticateCommand.OAUTH2_METHOD) {
|
||||
throw new ImapError.INVALID("Unexpected continuation request");
|
||||
}
|
||||
|
||||
// Continuation will be a Base64 encoded JSON blob and which
|
||||
// indicates a login failure. We don't really care about that
|
||||
// (do we?) though since once we acknowledge it with a
|
||||
// zero-length response the server will respond with an IMAP
|
||||
// error.
|
||||
return new ContinuationParameter(new uint8[0]);
|
||||
}
|
||||
|
||||
public override string to_string() {
|
||||
return "%s %s %s <token>".printf(
|
||||
tag.to_string(), this.name, this.method
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
53
src/engine/imap/parameter/imap-continuation-parameter.vala
Normal file
53
src/engine/imap/parameter/imap-continuation-parameter.vala
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright 2018 Michael Gratton <mike@vee.net>
|
||||
*
|
||||
* This software is licensed under the GNU Lesser General Public License
|
||||
* (version 2.1 or later). See the COPYING file in this distribution.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Represents a response to an IMAP continuation request.
|
||||
*
|
||||
* Do not use this if you need to send literal data as part of a
|
||||
* command, add it as a {@link LiteralParameter} to the command
|
||||
* instead.
|
||||
*
|
||||
* See [[http://tools.ietf.org/html/rfc3501#section-7.5]]
|
||||
*/
|
||||
|
||||
public class Geary.Imap.ContinuationParameter : Geary.Imap.Parameter {
|
||||
|
||||
|
||||
private uint8[] data;
|
||||
|
||||
|
||||
/**
|
||||
* Response to the continuation request.
|
||||
*
|
||||
* The given data will be sent to the server as-is. It should not
|
||||
* contain a trailing EOL.
|
||||
*/
|
||||
public ContinuationParameter(uint8[] data) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
public void serialize_continuation(Serializer ser)
|
||||
throws GLib.Error {
|
||||
ser.push_unquoted_string(
|
||||
new Memory.ByteBuffer.take(this.data, this.data.length).to_string()
|
||||
);
|
||||
ser.push_eol();
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
public override void serialize(Serializer ser, Tag tag)
|
||||
throws GLib.Error {
|
||||
serialize_continuation(ser);
|
||||
}
|
||||
|
||||
/** {@inheritDoc} */
|
||||
public override string to_string() {
|
||||
return new Memory.ByteBuffer.take(this.data, this.data.length).to_string();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -7,6 +7,8 @@
|
|||
public class Geary.Imap.Capabilities : Geary.GenericCapabilities {
|
||||
|
||||
|
||||
public const string AUTH = "AUTH";
|
||||
public const string AUTH_XOAUTH2 = "XOAUTH2";
|
||||
public const string CREATE_SPECIAL_USE = "CREATE-SPECIAL-USE";
|
||||
public const string COMPRESS = "COMPRESS";
|
||||
public const string DEFLATE_SETTING = "DEFLATE";
|
||||
|
|
|
|||
|
|
@ -632,7 +632,48 @@ public class Geary.Imap.ClientConnection : BaseObject {
|
|||
// by this signal, will want to tighten this up a bit in the future
|
||||
sent_command(cmd);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sends a reply to an unsolicited continuation request.
|
||||
*
|
||||
* Do not use this if you need to send literal data as part of a
|
||||
* command, add it as a {@link LiteralParameter} to the command
|
||||
* instead.
|
||||
*/
|
||||
public async void send_continuation_reply(ContinuationParameter reply,
|
||||
Cancellable? cancellable = null)
|
||||
throws Error {
|
||||
check_for_connection();
|
||||
// need to run this in critical section because Serializer requires it (don't want to be
|
||||
// pushing data while a flush_async() is occurring)
|
||||
int token = yield send_mutex.claim_async(cancellable);
|
||||
|
||||
Error? ser_err = null;
|
||||
try {
|
||||
// watch for disconnect while waiting for mutex
|
||||
if (ser != null) {
|
||||
reply.serialize_continuation(ser);
|
||||
} else {
|
||||
ser_err = new ImapError.NOT_CONNECTED("Send not allowed: connection in %s state",
|
||||
fsm.get_state_string(fsm.get_state()));
|
||||
}
|
||||
} catch (Error err) {
|
||||
debug("[%s] Error serializing command: %s", to_string(), err.message);
|
||||
ser_err = err;
|
||||
}
|
||||
|
||||
this.send_mutex.release(ref token);
|
||||
|
||||
if (ser_err != null) {
|
||||
send_failure(ser_err);
|
||||
|
||||
throw ser_err;
|
||||
}
|
||||
|
||||
// Reset flush timer so it only fires after n msec after last command pushed out to stream
|
||||
reschedule_flush_timeout();
|
||||
}
|
||||
|
||||
private void reschedule_flush_timeout() {
|
||||
unschedule_flush_timeout();
|
||||
|
||||
|
|
@ -646,7 +687,7 @@ public class Geary.Imap.ClientConnection : BaseObject {
|
|||
flush_timeout_id = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private bool on_flush_timeout() {
|
||||
do_flush_async.begin();
|
||||
|
||||
|
|
|
|||
|
|
@ -196,13 +196,14 @@ public class Geary.Imap.ClientSession : BaseObject {
|
|||
CLOSE_MAILBOX,
|
||||
LOGOUT,
|
||||
DISCONNECT,
|
||||
|
||||
|
||||
// server events
|
||||
CONNECTED,
|
||||
DISCONNECTED,
|
||||
RECV_STATUS,
|
||||
RECV_COMPLETION,
|
||||
|
||||
RECV_CONTINUATION,
|
||||
|
||||
// I/O errors
|
||||
RECV_ERROR,
|
||||
SEND_ERROR,
|
||||
|
|
@ -393,6 +394,7 @@ public class Geary.Imap.ClientSession : BaseObject {
|
|||
new Geary.State.Mapping(State.AUTHORIZING, Event.DISCONNECT, on_disconnect),
|
||||
new Geary.State.Mapping(State.AUTHORIZING, Event.RECV_STATUS, on_recv_status),
|
||||
new Geary.State.Mapping(State.AUTHORIZING, Event.RECV_COMPLETION, on_login_recv_completion),
|
||||
new Geary.State.Mapping(State.AUTHORIZING, Event.RECV_CONTINUATION, on_login_recv_continuation),
|
||||
new Geary.State.Mapping(State.AUTHORIZING, Event.SEND_ERROR, on_send_error),
|
||||
new Geary.State.Mapping(State.AUTHORIZING, Event.RECV_ERROR, on_recv_error),
|
||||
|
||||
|
|
@ -745,6 +747,7 @@ public class Geary.Imap.ClientSession : BaseObject {
|
|||
cx.send_failure.connect(on_network_send_error);
|
||||
cx.received_status_response.connect(on_received_status_response);
|
||||
cx.received_server_data.connect(on_received_server_data);
|
||||
cx.received_continuation_response.connect(on_received_continuation_response);
|
||||
cx.received_bytes.connect(on_received_bytes);
|
||||
cx.received_bad_response.connect(on_received_bad_response);
|
||||
cx.recv_closed.connect(on_received_closed);
|
||||
|
|
@ -771,6 +774,7 @@ public class Geary.Imap.ClientSession : BaseObject {
|
|||
cx.send_failure.disconnect(on_network_send_error);
|
||||
cx.received_status_response.disconnect(on_received_status_response);
|
||||
cx.received_server_data.disconnect(on_received_server_data);
|
||||
cx.received_continuation_response.disconnect(on_received_continuation_response);
|
||||
cx.received_bytes.disconnect(on_received_bytes);
|
||||
cx.received_bad_response.disconnect(on_received_bad_response);
|
||||
cx.recv_closed.disconnect(on_received_closed);
|
||||
|
|
@ -855,7 +859,33 @@ public class Geary.Imap.ClientSession : BaseObject {
|
|||
throw new ImapError.UNAUTHENTICATED("No credentials provided for account: %s", credentials.to_string());
|
||||
}
|
||||
|
||||
LoginCommand cmd = new LoginCommand(credentials.user, credentials.token);
|
||||
Command? cmd = null;
|
||||
switch (credentials.supported_method) {
|
||||
case Geary.Credentials.Method.PASSWORD:
|
||||
cmd = new LoginCommand(
|
||||
credentials.user, credentials.token
|
||||
);
|
||||
break;
|
||||
|
||||
case Geary.Credentials.Method.OAUTH2:
|
||||
if (!capabilities.has_setting(Capabilities.AUTH,
|
||||
Capabilities.AUTH_XOAUTH2)) {
|
||||
throw new ImapError.UNAUTHENTICATED(
|
||||
"OAuth2 authentication not supported for %s", to_string()
|
||||
);
|
||||
}
|
||||
cmd = new AuthenticateCommand.oauth2(
|
||||
credentials.user, credentials.token
|
||||
);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new ImapError.UNAUTHENTICATED(
|
||||
"Credentials method %s not supported for: %s",
|
||||
credentials.supported_method.to_string(),
|
||||
to_string()
|
||||
);
|
||||
}
|
||||
|
||||
MachineParams params = new MachineParams(cmd);
|
||||
fsm.issue(Event.LOGIN, null, params);
|
||||
|
|
@ -1038,14 +1068,13 @@ public class Geary.Imap.ClientSession : BaseObject {
|
|||
|
||||
private uint on_login(uint state, uint event, void *user, Object? object) {
|
||||
MachineParams params = (MachineParams) object;
|
||||
|
||||
assert(params.cmd is LoginCommand);
|
||||
|
||||
if (!reserve_state_change_cmd(params, state, event))
|
||||
return state;
|
||||
|
||||
|
||||
return State.AUTHORIZING;
|
||||
}
|
||||
|
||||
|
||||
private uint on_logging_in(uint state, uint event, void *user, Object? object) {
|
||||
MachineParams params = (MachineParams) object;
|
||||
|
||||
|
|
@ -1074,7 +1103,36 @@ public class Geary.Imap.ClientSession : BaseObject {
|
|||
return State.NOAUTH;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private uint on_login_recv_continuation(uint state,
|
||||
uint event,
|
||||
void *user,
|
||||
Object? object) {
|
||||
ContinuationResponse response = (ContinuationResponse) object;
|
||||
AuthenticateCommand auth = this.state_change_cmd as AuthenticateCommand;
|
||||
if (auth != null) {
|
||||
ContinuationParameter? reply = null;
|
||||
try {
|
||||
reply = auth.continuation_requested(response);
|
||||
} catch (ImapError err) {
|
||||
debug("[%s] Error handling login continuation request: %s",
|
||||
to_string(), err.message);
|
||||
}
|
||||
|
||||
if (reply != null) {
|
||||
// We have to handle the continuation request anyway,
|
||||
// so just send an empty one.
|
||||
reply = new ContinuationParameter(new uint8[0]);
|
||||
}
|
||||
|
||||
// XXX Not calling yield here is a nasty hack? Need to get
|
||||
// a cancellable to this somehow, too.
|
||||
this.cx.send_continuation_reply.begin(reply, null);
|
||||
}
|
||||
|
||||
return State.AUTHORIZING;
|
||||
}
|
||||
|
||||
//
|
||||
// keepalives (nop idling to keep the session alive and to periodically receive notifications
|
||||
// of changes)
|
||||
|
|
@ -1284,6 +1342,7 @@ public class Geary.Imap.ClientSession : BaseObject {
|
|||
//
|
||||
// TODO: Convert commands into proper calls to avoid throwing an exception
|
||||
if (cmd.has_name(LoginCommand.NAME)
|
||||
|| cmd.has_name(AuthenticateCommand.NAME)
|
||||
|| cmd.has_name(LogoutCommand.NAME)
|
||||
|| cmd.has_name(SelectCommand.NAME)
|
||||
|| cmd.has_name(ExamineCommand.NAME)
|
||||
|
|
@ -1873,6 +1932,15 @@ public class Geary.Imap.ClientSession : BaseObject {
|
|||
}
|
||||
}
|
||||
|
||||
private void on_received_continuation_response(ContinuationResponse response) {
|
||||
this.last_seen = GLib.get_real_time();
|
||||
|
||||
// reschedule keepalive (traffic seen on channel)
|
||||
schedule_keepalive();
|
||||
|
||||
fsm.issue(Event.RECV_CONTINUATION, null, response, null);
|
||||
}
|
||||
|
||||
private void on_received_bytes(size_t bytes) {
|
||||
this.last_seen = GLib.get_real_time();
|
||||
|
||||
|
|
|
|||
|
|
@ -92,6 +92,7 @@ geary_engine_vala_sources = files(
|
|||
'imap/api/imap-folder-session.vala',
|
||||
'imap/api/imap-session-object.vala',
|
||||
'imap/command/imap-append-command.vala',
|
||||
'imap/command/imap-authenticate-command.vala',
|
||||
'imap/command/imap-capability-command.vala',
|
||||
'imap/command/imap-close-command.vala',
|
||||
'imap/command/imap-command.vala',
|
||||
|
|
@ -136,6 +137,7 @@ geary_engine_vala_sources = files(
|
|||
'imap/message/imap-uid.vala',
|
||||
'imap/message/imap-uid-validity.vala',
|
||||
'imap/parameter/imap-atom-parameter.vala',
|
||||
'imap/parameter/imap-continuation-parameter.vala',
|
||||
'imap/parameter/imap-list-parameter.vala',
|
||||
'imap/parameter/imap-literal-parameter.vala',
|
||||
'imap/parameter/imap-nil-parameter.vala',
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue