From: Eric Gillespie Date: Tue, 9 Dec 2014 07:20:01 +0000 (-0800) Subject: Implement OAuth 2.0 [1] for XOAUTH2 in SMTP [2] and POP3 [3]. X-Git-Url: https://diplodocus.org/git/nmh/commitdiff_plain/803f254122dc757db104a4c36cf98b726be004be?ds=sidebyside;hp=-c Implement OAuth 2.0 [1] for XOAUTH2 in SMTP [2] and POP3 [3]. Google defined XOAUTH2 for SMTP, and that's what we use here. If other providers implement XOAUTH2 or some similar OAuth-based SMTP authentication protocol, it should be simple to extend this. [1] https://tools.ietf.org/html/rfc6749 [2] https://developers.google.com/gmail/xoauth2_protocol [3] http://googleappsdeveloper.blogspot.com/2014/10/updates-on-authentication-for-gmail.html Technically, XOAUTH2 is a SASL auth mechanism, but the implementation is so trivial, I can't justify the code complexity or additional dependency requirement of using Cyrus SASL for this. So it's completely separate. Changes: - New dependencies: - jsmn (JSON processing library) bundled directly rather than linked to as an external library because there is no clear winner among JSON libraries for C and this one is tiny - libcurl is nearly ubiquitous and too heavy-weight to bundle, so link to the library the user must install separately - Add oauth.h / oauth.c which do almost all the work, with quite a bit of help from curl and jsmn. - Add new mhlogin program to authorize nmh to use the Gmail account and store the access and refresh tokens. - Add new user_agent global to version.c (version.sh); not too happy with such a generic name, but the others had no mh_ prefix or anything... - Add XOAUTH2 support to: mts/smtp/smtp.c uip/post.c uip/send.c uip/popsbr.c uip/inc.c uip/msgchk.c - Split duplicated serving code out of fakepop.c and fakesmtp.c to new server.c and also use that for new fakehttp.c. - Add XOAUTH2 support to fakepop.c and fakesmtp.c. --- 803f254122dc757db104a4c36cf98b726be004be diff --git a/.gitignore b/.gitignore index 4e9297db..27038108 100644 --- a/.gitignore +++ b/.gitignore @@ -80,6 +80,7 @@ a.out.dSYM/ /uip/mhfixmsg /uip/mhl /uip/mhlist +/uip/mhlogin /uip/mhn /uip/mhparam /uip/mhpath @@ -110,6 +111,7 @@ a.out.dSYM/ /uip/whatnow /uip/whom /uip/*.exe +/test/fakehttp /test/fakepop /test/fakesmtp /test/getcanon diff --git a/Makefile.am b/Makefile.am index 1cc08753..59269d51 100644 --- a/Makefile.am +++ b/Makefile.am @@ -34,6 +34,9 @@ nmhlibexecdir = @libexecdir@/nmh ## nmh _does_ have a test suite! ## TESTS_ENVIRONMENT = MH_OBJ_DIR="@abs_builddir@" \ + MH_VERSION="$(VERSION)" \ + OAUTH_SUPPORT='@OAUTH_SUPPORT@' \ + CURL_USER_AGENT='@CURL_USER_AGENT@' \ MH_TEST_DIR="@abs_builddir@/test/testdir" \ nmhlibexecdir="$(nmhlibexecdir)" bindir="$(bindir)" \ mandir="$(mandir)" nmhetcdir="$(nmhetcdir)" \ @@ -81,6 +84,8 @@ TESTS = test/ali/test-ali test/anno/test-anno \ test/mhshow/test-subpart test/mhshow/test-msg-buffer-boundaries \ test/mhstore/test-mhstore test/mkstemp/test-mkstemp \ test/new/test-basic test/pick/test-pick test/pick/test-stderr \ + test/oauth/test-mhlogin test/oauth/test-send \ + test/oauth/test-inc test/oauth/test-share \ test/post/test-post-aliases test/post/test-post-basic \ test/post/test-post-multiple test/post/test-post-bcc \ test/post/test-post-dcc test/post/test-post-fcc \ @@ -101,7 +106,7 @@ TESTS = test/ali/test-ali test/anno/test-anno \ check_SCRIPTS = test/common.sh check_PROGRAMS = test/getfullname test/getcanon test/fakepop test/fakesmtp \ - test/getcwidth + test/getcwidth test/fakehttp DISTCHECK_CONFIGURE_FLAGS = DISABLE_SETGID_MAIL=1 ## @@ -147,7 +152,7 @@ bin_PROGRAMS = uip/ali uip/anno uip/burst uip/comp uip/dist uip/flist \ uip/mhparam uip/mhpath uip/mhshow uip/mhstore uip/msgchk \ uip/new uip/packf uip/pick uip/prompter uip/refile \ uip/repl uip/rmf uip/rmm uip/scan uip/send uip/show uip/sortm \ - uip/whatnow uip/whom + uip/whatnow uip/whom uip/mhlogin bin_SCRIPTS = uip/mhmail etc/sendfiles @@ -182,7 +187,8 @@ noinst_HEADERS = h/addrsbr.h h/aliasbr.h h/crawl_folders.h h/dropsbr.h \ h/mh.h h/mhcachesbr.h h/mhparse.h h/mime.h \ h/mts.h h/nmh.h h/picksbr.h h/popsbr.h h/prototypes.h \ h/rcvmail.h h/scansbr.h h/signals.h h/tws.h h/utils.h \ - mts/smtp/smtp.h sbr/ctype-checked.h + mts/smtp/smtp.h sbr/ctype-checked.h h/oauth.h \ + thirdparty/jsmn/jsmn.h ## ## Extra files we need to install in various places @@ -239,7 +245,7 @@ man_MANS = man/ali.1 man/anno.1 man/ap.8 man/burst.1 man/comp.1 \ man/prompter.1 man/rcvdist.1 man/rcvpack.1 man/rcvstore.1 \ man/rcvtty.1 man/refile.1 man/repl.1 man/rmf.1 man/rmm.1 \ man/scan.1 man/send.1 man/sendfiles.1 man/show.1 man/slocal.1 \ - man/sortm.1 man/unseen.1 man/whatnow.1 man/whom.1 + man/sortm.1 man/unseen.1 man/whatnow.1 man/whom.1 man/mhlogin.1 ## ## Sources for our man pages @@ -261,7 +267,7 @@ man_SRCS = man/ali.man man/anno.man man/ap.man man/burst.man man/comp.man \ man/rcvstore.man man/rcvtty.man man/refile.man man/repl.man \ man/rmf.man man/rmm.man man/scan.man man/send.man \ man/sendfiles.man man/show.man man/slocal.man man/sortm.man \ - man/unseen.man man/whatnow.man man/whom.man + man/unseen.man man/whatnow.man man/whom.man man/mhlogin.man ## ## Files we need to include in the distribution which aren't found by @@ -278,7 +284,9 @@ EXTRA_DIST = autogen.sh config/version.sh sbr/sigmsg.awk etc/mts.conf.in \ test/mhbuild/somebinary \ test/mhbuild/nulls \ test/mhbuild/textplain \ - test/post/test-post-common.sh test/valgrind.supp uip/mhmail \ + test/post/test-post-common.sh test/valgrind.supp \ + test/oauth/common.sh \ + uip/mhmail \ SPECS/nmh.spec SPECS/build-nmh-cygwin $(man_SRCS) ## @@ -318,7 +326,7 @@ uip_forw_SOURCES = uip/forw.c uip/whatnowproc.c uip/whatnowsbr.c uip/sendsbr.c \ uip_forw_LDADD = $(LDADD) $(READLINELIB) $(TERMLIB) $(ICONVLIB) $(POSTLINK) uip_inc_SOURCES = uip/inc.c uip/scansbr.c uip/dropsbr.c uip/popsbr.c -uip_inc_LDADD = $(LDADD) $(TERMLIB) $(ICONVLIB) $(SASLLIB) $(POSTLINK) +uip_inc_LDADD = $(LDADD) $(TERMLIB) $(ICONVLIB) $(SASLLIB) $(POSTLINK) $(CURLLIB) uip_install_mh_SOURCES = uip/install-mh.c uip_install_mh_LDADD = $(LDADD) $(POSTLINK) @@ -362,7 +370,7 @@ uip_mhstore_SOURCES = uip/mhstore.c uip/mhparse.c uip/mhcachesbr.c \ uip_mhstore_LDADD = $(LDADD) $(TERMLIB) $(ICONVLIB) $(POSTLINK) uip_msgchk_SOURCES = uip/msgchk.c uip/popsbr.c -uip_msgchk_LDADD = $(LDADD) $(SASLLIB) $(POSTLINK) +uip_msgchk_LDADD = $(LDADD) $(SASLLIB) $(POSTLINK) $(CURLLIB) uip_new_SOURCES = uip/new.c uip_new_LDADD = $(LDADD) $(POSTLINK) @@ -394,7 +402,7 @@ uip_scan_LDADD = $(LDADD) $(TERMLIB) $(ICONVLIB) $(POSTLINK) uip_send_SOURCES = uip/send.c uip/sendsbr.c uip/annosbr.c \ uip/distsbr.c -uip_send_LDADD = $(LDADD) $(POSTLINK) +uip_send_LDADD = $(LDADD) $(POSTLINK) $(CURLLIB) uip_show_SOURCES = uip/show.c uip/mhlsbr.c uip_show_LDADD = $(LDADD) $(TERMLIB) $(ICONVLIB) $(POSTLINK) @@ -428,6 +436,9 @@ uip_fmttest_LDADD = $(LDADD) $(TERMLIB) $(ICONVLIB) $(POSTLINK) uip_mhl_SOURCES = uip/mhl.c uip/mhlsbr.c uip_mhl_LDADD = $(LDADD) $(TERMLIB) $(ICONVLIB) $(POSTLINK) +uip_mhlogin_SOURCES = uip/mhlogin.c +uip_mhlogin_LDADD = mts/libmts.a $(LDADD) $(CURLLIB) $(POSTLINK) + uip_mkstemp_SOURCES = uip/mkstemp.c uip_mkstemp_LDADD = $(LDADD) $(POSTLINK) @@ -459,12 +470,15 @@ test_getfullname_LDADD = $(LDADD) $(POSTLINK) test_getcanon_SOURCES = test/getcanon.c test_getcanon_LDADD = $(POSTLINK) -test_fakepop_SOURCES = test/fakepop.c +test_fakepop_SOURCES = test/fakepop.c test/server.c test_fakepop_LDADD = $(POSTLINK) -test_fakesmtp_SOURCES = test/fakesmtp.c +test_fakesmtp_SOURCES = test/fakesmtp.c test/server.c test_fakesmtp_LDADD = $(POSTLINK) +test_fakehttp_SOURCES = test/fakehttp.c test/server.c +test_fakehttp_LDADD = $(POSTLINK) + test_getcwidth_SOURCES = test/getcwidth.c test_getcwidth_LDADD = $(POSTLINK) @@ -587,7 +601,8 @@ sbr_libmh_a_SOURCES = sbr/addrsbr.c sbr/ambigsw.c sbr/atooi.c sbr/arglist.c \ sbr/uprf.c sbr/vfgets.c \ sbr/mf.c sbr/utils.c sbr/ctype-checked.c \ sbr/m_mktemp.c sbr/getansreadline.c sbr/vector.c \ - config/config.c config/version.c + config/config.c config/version.c sbr/oauth.c \ + thirdparty/jsmn/jsmn.c ## ## Because these files use the definitions in the libmh rule below, diff --git a/config/version.sh b/config/version.sh index bf95447d..a0105e36 100755 --- a/config/version.sh +++ b/config/version.sh @@ -47,3 +47,4 @@ else echo "char *version_str = \"nmh-$VERSION [compiled on $HOSTNAME at `date`]\";" fi echo "char *version_num = \"nmh-$VERSION\";" +echo "char *user_agent = \"nmh/$VERSION\";" diff --git a/configure.ac b/configure.ac index e0a96f5d..507e4eae 100644 --- a/configure.ac +++ b/configure.ac @@ -40,6 +40,15 @@ AS_IF([test x"$with_cyrus_sasl" != x -a x"$with_cyrus_sasl" != x"no"],[ AC_MSG_WARN([Please pass the appropriate arguments to CPPFLAGS/LDFLAGS])]) sasl_support=yes], [sasl_support=no]) +dnl Do you want client-side support for using OAuth2 for SMTP authentication? +AC_ARG_WITH([oauth], AS_HELP_STRING([--with-oauth], + [Enable OAuth2 support in SMTP auth])) +AS_IF([test x"$with_oauth" != x -a x"$with_oauth" != x"no"],[ + AC_DEFINE([OAUTH_SUPPORT], [1], + [Support OAuth2 in SMTP auth.])dnl + OAUTH_SUPPORT=1; oauth_support=yes], [OAUTH_SUPPORT=0; oauth_support=no]) +AC_SUBST(OAUTH_SUPPORT) + dnl Do you want client-side support for encryption with TLS? AC_ARG_WITH([tls], AS_HELP_STRING([--with-tls], [Enable TLS support])) AS_IF([test x"$with_tls" != x"no"],[ @@ -501,6 +510,22 @@ AS_IF([test x"$tls_support" = x"yes"],[ [TLSLIB=]) AC_SUBST([TLSLIB]) +dnl ----------------- +dnl CHECK FOR CURL +dnl ----------------- +AS_IF([test x"$OAUTH_SUPPORT" = x"1"],[ + AC_PATH_PROG([curl_config], [curl-config]) + AC_CHECK_HEADER([curl/curl.h], [], [AC_MSG_ERROR([curl/curl.h not found])]) + AC_CHECK_LIB([curl], [curl_easy_init], [CURLLIB="`$curl_config --libs`"], + [AC_MSG_ERROR([curl library not found])],[$CURLLIB]) + CURL_USER_AGENT=`$curl_config --version | sed 's| |/|'` + ], + [CURLLIB= + CURL_USER_AGENT= +]) +AC_SUBST([CURLLIB]) +AC_SUBST([CURL_USER_AGENT]) + dnl ---------------- dnl CHECK FLEX FIXUP dnl ---------------- @@ -590,6 +615,7 @@ spool default locking type : ${with_locking} default smtp servers : ${smtpservers} SASL support : ${sasl_support} TLS support : ${tls_support} +OAuth support : ${oauth_support} ])])dnl dnl --------------- diff --git a/h/mh.h b/h/mh.h index b8c60ca8..71315b6e 100644 --- a/h/mh.h +++ b/h/mh.h @@ -490,6 +490,7 @@ extern char *sendproc; extern char *showmimeproc; extern char *showproc; extern char *usequence; +extern char *user_agent; extern char *version_num; extern char *version_str; extern char *whatnowproc; diff --git a/h/oauth.h b/h/oauth.h new file mode 100644 index 00000000..a49cb4ac --- /dev/null +++ b/h/oauth.h @@ -0,0 +1,221 @@ +/* + * Implementation of OAuth 2.0 [1] for XOAUTH2 in SMTP [2] and POP3 [3]. + * + * Google defined XOAUTH2 for SMTP, and that's what we use here. If other + * providers implement XOAUTH2 or some similar OAuth-based SMTP authentication + * protocol, it should be simple to extend this. + * + * [1] https://tools.ietf.org/html/rfc6749 + * [2] https://developers.google.com/gmail/xoauth2_protocol + * [3] http://googleappsdeveloper.blogspot.com/2014/10/updates-on-authentication-for-gmail.html + * + * Presumably [2] should document POP3 and that is an over-sight. As it stands, + * that blog post is the closest we have to documentation. + * + * According to [1] 2.1 Client Types, this is a "native application", a + * "public" client. + * + * To summarize the flow: + * + * 1. User runs mhlogin which prints a URL the user must visit, and prompts for + * a code retrieved from that page. + * + * 2. User vists this URL in browser, signs in with some Google account, and + * copies and pastes the resulting code back to mhlogin. + * + * 3. mhlogin does HTTP POST to Google to exchange the user-provided code for a + * short-lived access token and a long-lived refresh token. + * + * 4. send uses the access token in SMTP auth if not expired. If it is expired, + * it does HTTP POST to Google including the refresh token and gets back a + * new access token (and possibly refresh token). If the refresh token has + * become invalid (e.g. if the user took some reset action on the Google + * account), the user must use mhlogin again, then re-run send. + */ + +typedef enum { + /* error loading profile */ + MH_OAUTH_BAD_PROFILE = OK + 1, + + /* error initializing libcurl */ + MH_OAUTH_CURL_INIT, + + /* local error initializing HTTP request */ + MH_OAUTH_REQUEST_INIT, + + /* error executing HTTP POST request */ + MH_OAUTH_POST, + + /* HTTP response body is too big. */ + MH_OAUTH_RESPONSE_TOO_BIG, + + /* Can't process HTTP response body. */ + MH_OAUTH_RESPONSE_BAD, + + /* The authorization server rejected the grant (authorization code or + * refresh token); possibly the user entered a bad code, or the refresh + * token has become invalid, etc. */ + MH_OAUTH_BAD_GRANT, + + /* HTTP server indicates something is wrong with our request. */ + MH_OAUTH_REQUEST_BAD, + + /* Attempting to refresh an access token without a refresh token. */ + MH_OAUTH_NO_REFRESH, + + /* error loading serialized credentials */ + MH_OAUTH_CRED_FILE +} mh_oauth_err_code; + +typedef struct mh_oauth_ctx mh_oauth_ctx; + +typedef struct mh_oauth_cred mh_oauth_cred; + +/* + * Do the complete dance for XOAUTH2 as used by POP3 and SMTP. + * + * Load tokens for svc from disk, refresh if necessary, and return the + * base64-encoded client response. + * + * If refreshing, writes freshened tokens to disk. + * + * Exits via adios on any error. + */ +char * +mh_oauth_do_xoauth(const char *user, const char *svc, FILE *log); + +/* + * Allocate and initialize a new OAuth context. + * + * Caller must call mh_oauth_free(ctx) when finished, even on error. + * + * svc_name must point to a null-terminated string identifying the service + * provider. Support for "gmail" is built-in; anything else must be defined in + * the user's profile. The profile can also override "gmail" settings. + * + * Accesses global m_defs via context_find. + * + * On error, return FALSE and set an error in ctx; ctx is always allocated. + */ +boolean +mh_oauth_new(mh_oauth_ctx **ctx, const char *svc_name); + +/* + * Free all resources associated with ctx. + */ +void +mh_oauth_free(mh_oauth_ctx *ctx); + +/* + * Return null-terminated human-readable name of the service, e.g. "Gmail". + * + * Never returns NULL. + */ +const char * +mh_oauth_svc_display_name(const mh_oauth_ctx *ctx); + +/* + * Enable logging for subsequent operations on ctx. + * + * log must not be closed until after mh_oauth_free. + * + * For all HTTP requests, the request is logged with each line prefixed with + * "< ", and the response with "> ". Other messages are prefixed with "* ". + */ +void +mh_oauth_log_to(FILE *log, mh_oauth_ctx *ctx); + +/* + * Return the error code after some function indicated an error. + * + * Must not be called if an error was not indicated. + */ +mh_oauth_err_code +mh_oauth_get_err_code(const mh_oauth_ctx *ctx); + +/* + * Return null-terminated error message after some function indicated an error. + * + * Never returns NULL, but must not be called if an error was not indicated. + */ +const char * +mh_oauth_get_err_string(mh_oauth_ctx *ctx); + +/* + * Return the null-terminated URL the user needs to visit to authorize access. + * + * URL may be invalidated by subsequent calls to mh_oauth_get_authorize_url, + * mh_oauth_authorize, or mh_oauth_refresh. + * + * On error, return NULL. + */ +const char * +mh_oauth_get_authorize_url(mh_oauth_ctx *ctx); + +/* + * Exchange code provided by the user for access (and maybe refresh) token. + * + * On error, return NULL. + */ +mh_oauth_cred * +mh_oauth_authorize(const char *code, mh_oauth_ctx *ctx); + +/* + * Refresh access (and maybe refresh) token if refresh token present. + * + * On error, return FALSE and leave cred untouched. + */ +boolean +mh_oauth_refresh(mh_oauth_cred *cred); + +/* + * Return whether access token is present and not expired at time T. + */ +boolean +mh_oauth_access_token_valid(time_t t, const mh_oauth_cred *cred); + +/* + * Free all resources associated with cred. + */ +void +mh_oauth_cred_free(mh_oauth_cred *cred); + +/* + * Return the null-terminated file name for storing this service's OAuth tokens. + * + * Accesses global m_defs via context_find. + * + * Never returns NULL. + */ +const char * +mh_oauth_cred_fn(mh_oauth_ctx *ctx); + +/* + * Serialize OAuth tokens to file. + * + * On error, return FALSE. + */ +boolean +mh_oauth_cred_save(FILE *fp, mh_oauth_cred *cred); + +/* + * Load OAuth tokens from file. + * + * Calls m_getfld(), which writes to stderr with advise(). + * + * On error, return NULL. + */ +mh_oauth_cred * +mh_oauth_cred_load(FILE *fp, mh_oauth_ctx *ctx); + +/* + * Return null-terminated SASL client response for XOAUTH2 from access token. + * + * Store the length in res_len. + * + * Must not be called except after successful mh_oauth_access_token_valid or + * mh_oauth_refresh call; i.e. must have a valid access token. + */ +const char * +mh_oauth_sasl_client_response(size_t *res_len, + const char *user, const mh_oauth_cred *cred); diff --git a/h/popsbr.h b/h/popsbr.h index fc06f0b9..3fb4179d 100644 --- a/h/popsbr.h +++ b/h/popsbr.h @@ -3,7 +3,8 @@ * popsbr.h -- header for POP client subroutines */ -int pop_init (char *, char *, char *, char *, char *, int, int, char *); +int pop_init (char *, char *, char *, char *, char *, int, int, char *, + const char *); int pop_fd (char *, int, char *, int); int pop_stat (int *, int *); int pop_retr (int, int (*)(char *)); diff --git a/h/prototypes.h b/h/prototypes.h index a338fd52..5c104a7e 100644 --- a/h/prototypes.h +++ b/h/prototypes.h @@ -18,9 +18,9 @@ char *etcpath(char *); struct msgs_array; void add_profile_entry (const char *, const char *); -void adios (char *, char *, ...) NORETURN; +void adios (char *, const char *, ...) NORETURN; void admonish (char *, char *, ...); -void advertise (char *, char *, char *, va_list); +void advertise (char *, char *, const char *, va_list); void advise (char *, char *, ...); char **argsplit (char *, char **, int *); void argsplit_msgarg (struct msgs_array *, char *, char **); @@ -287,7 +287,7 @@ void print_version (char *); void push (void); char *pwd (void); char *r1bindex(char *, int); -void readconfig (struct node **, FILE *, char *, int); +void readconfig (struct node **, FILE *, const char *, int); int refile (char **, char *); void ruserpass (char *, char **, char **); int remdir (char *); diff --git a/man/inc.man b/man/inc.man index 61f6f844..f72b1f21 100644 --- a/man/inc.man +++ b/man/inc.man @@ -1,4 +1,4 @@ -.TH INC %manext1% "April 18, 2014" "%nmhversion%" +.TH INC %manext1% "November 25, 2014" "%nmhversion%" .\" .\" %nmhwarning% .\" @@ -37,6 +37,8 @@ inc \- incorporate new mail .RB [ \-sasl " | " \-nosasl ] .RB [ \-saslmech .IR mechanism ] +.RB [ \-oauth +.IR service ] .RB [ \-snoop ] .RB [ \-version ] .RB [ \-help ] @@ -242,7 +244,9 @@ the user's maildrop from the POP service host to the named file. For debugging purposes, you may give the switch .BR \-snoop , which will allow you to watch the POP transaction take place -between you and the POP server. +between you and the POP server. If +.B \-oauth +is used, the HTTP transaction is also shown. .PP If .B nmh @@ -264,6 +268,29 @@ Encrypted traffic is labelled with `(encrypted)' and `(decrypted)' when viewing the POP transaction with the .B \-snoop switch. +.PP +If +.B nmh +has been compiled with OAuth support, the +.B \-oauth +switch will enable OAuth authentication. The +.B \-user +switch must be used, and the +.I user-name +must be an email address the user has for that service. Before using this, +the user must authorize nmh by running +.B mhlogin +and grant authorization to that account. Only +.B -oauth +.I gmail +is supported. See the +.B mhlogin +man page for more details. +.PP +Gmail only supports POP3 over TLS, but +.B inc +has no TLS support. To work around this, use something like +.B -proxy 'openssl s_client -connect %h:995 -CAfile /etc/ssl/certs/ca-certificates.crt -quiet' .SH FILES .PD 0 .TP 20 @@ -302,6 +329,7 @@ To name sequences denoting unseen messages. .IR scan (1), .IR mh\-mail (5), .IR mh\-profile (5), +.IR mhlogin (1), .IR post (8), .IR rcvstore (1) .SH DEFAULTS diff --git a/man/mhlogin.man b/man/mhlogin.man new file mode 100644 index 00000000..0651fdca --- /dev/null +++ b/man/mhlogin.man @@ -0,0 +1,63 @@ +.\" +.\" %nmhwarning% +.\" +.TH SEND %manext1% "November 25, 2014" "%nmhversion%" +.SH NAME +mhlogin \- login to external (OAuth) services +.SH SYNOPSIS +.HP 5 +.na +.B mhlogin +.RB \-oauth +.IR service +.RB [ \-snoop ] +.RB [ \-version ] +.RB [ \-help ] +.ad +.SH DESCRIPTION +.B Mhlogin +currently only supports OAuth for Gmail. Run +.B mhlogin +.B -oauth +.I gmail +and load the printed URL in your browser. Login to a Gmail account, grant +authorization, and copy and paste the code into the +.B mhlogin +prompt. Be sure to use the same account with the +.B -user +switch to +.B send +.PP +The +.B \-snoop +switch can be used to view the HTTP transaction. +.PP +All parameters configuring the service may be overridden by profile components, +and even though only Gmail is supported out of the box, the user can define +new services entirely in the profile. Profile components are prefixed by +.I +oauth- +.I +service- +for example +.I oauth-gmail-credential-file +which specifies where +.B mhlogin +should write credentials and where +.B send +should read them. +.SH "PROFILE COMPONENTS" +.fc ^ ~ +.nf +.ta 2.4i +.ta \w'ExtraBigProfileName 'u +^oauth-gmail-credential-file:~^oauth-gmail +^oauth-gmail-client_id:~^nmh project client_id +^oauth-gmail-client_secret:~^nmh project client_secret +^oauth-gmail-auth_endpoint:~^https://accounts.google.com/o/oauth2/auth +^oauth-gmail-redirect_uri:~^urn:ietf:wg:oauth:2.0:oob +^oauth-gmail-token_endpoint:~^https://accounts.google.com/o/oauth2/token +^oauth-gmail-scope:~^https://mail.google.com/ +.fi +.SH "SEE ALSO" +.IR send (1) diff --git a/man/msgchk.man b/man/msgchk.man index 0d5d6c98..5b02c52c 100644 --- a/man/msgchk.man +++ b/man/msgchk.man @@ -1,4 +1,4 @@ -.TH MSGCHK %manext1% "April 14, 2013" "%nmhversion%" +.TH MSGCHK %manext1% "November 25, 2014" "%nmhversion%" .\" .\" %nmhwarning% .\" @@ -20,6 +20,8 @@ all/mail/nomail ] .RB [ \-sasl ] .RB [ \-saslmech .IR mechanism ] +.RB [ \-oauth +.IR service ] .RB [ \-snoop ] .RI [ users \&... ] @@ -97,7 +99,9 @@ For debugging purposes, there is also a switch .BR \-snoop , which will allow you to watch the POP transaction take place between you and the -POP server. +POP server. If +.B \-oauth +is used, the HTTP transaction is also shown. .PP If .B nmh @@ -113,13 +117,36 @@ mh-profile(5) man page). The switch can be used to select a particular SASL mechanism. .PP If SASL authentication is successful, -.B inc +.B msgchk will attempt to negotiate a security layer for session encryption. Encrypted traffic is labelled with `(encrypted)' and `(decrypted)' when viewing the POP transaction with the .B \-snoop switch. +.PP +If +.B nmh +has been compiled with OAuth support, the +.B \-oauth +switch will enable OAuth authentication. The +.B \-user +switch must be used, and the +.I user-name +must be an email address the user has for that service. Before using this, +the user must authorize nmh by running +.B mhlogin +and grant authorization to that account. Only +.B -oauth +.I gmail +is supported. See the +.B mhlogin +man page for more details. +.PP +Gmail only supports POP3 over TLS, but +.B msgchk +has no TLS support. To work around this, use something like +.B -proxy 'openssl s_client -connect %h:995 -CAfile /etc/ssl/certs/ca-certificates.crt -quiet' .SH FILES .fc ^ ~ .nf diff --git a/man/send.man b/man/send.man index 83333673..29e6e243 100644 --- a/man/send.man +++ b/man/send.man @@ -1,7 +1,7 @@ .\" .\" %nmhwarning% .\" -.TH SEND %manext1% "July 8, 2014" "%nmhversion%" +.TH SEND %manext1% "November 25, 2014" "%nmhversion%" .SH NAME send \- send a message .SH SYNOPSIS @@ -25,6 +25,8 @@ send \- send a message .RB [ \-msgid " | " \-nomsgid ] .RB [ \-messageid .IR localname " | " random ] +.RB [ \-oauth +.IR service ] .RB [ \-push " | " \-nopush ] .RB [ \-split .IR seconds ] @@ -378,7 +380,9 @@ entry). The .B \-snoop switch can be used to view the SMTP transaction. (Beware that the SMTP transaction may contain authentication information either in -plaintext or easily decoded base64.) +plaintext or easily decoded base64.) If +.B \-oauth +is used, the HTTP transaction is also shown. .PP If .B nmh @@ -416,6 +420,24 @@ underlying SASL mechanism. A value of 0 disables encryption. .PP If .B nmh +has been compiled with OAuth support, the +.B \-oauth +switch will enable OAuth authentication. The +.B \-user +switch must be used, and the +.I user-name +must be an email address the user has for that service. Before using this, +the user must authorize nmh by running +.B mhlogin +and grant authorization to that account. Only +.B -oauth +.I gmail +is supported. See the +.B mhlogin +man page for more details. +.PP +If +.B nmh has been compiled with TLS support, the .B \-tls and @@ -478,6 +500,7 @@ for more information. .IR forw (1), .IR mhbuild (1), .IR mhparam (1), +.IR mhlogin (1), .IR repl (1), .IR whatnow (1), .IR mh\-alias (5), diff --git a/mts/smtp/smtp.c b/mts/smtp/smtp.c index 873e0cd8..39cb713a 100644 --- a/mts/smtp/smtp.c +++ b/mts/smtp/smtp.c @@ -76,9 +76,7 @@ #define SM_DOT 600 /* see above */ #define SM_QUIT 30 #define SM_CLOS 10 -#ifdef CYRUS_SASL #define SM_AUTH 45 -#endif /* CYRUS_SASL */ static int sm_addrs = 0; static int sm_alarmed = 0; @@ -153,7 +151,7 @@ static char *EHLOkeys[MAXEHLO + 1]; * static prototypes */ static int smtp_init (char *, char *, char *, int, int, int, int, int, - char *, char *, int); + char *, char *, const char *, int); static int sendmail_init (char *, char *, int, int, int, int, int, char *, char *); @@ -173,6 +171,7 @@ static int sm_fputs(char *); static int sm_fputc(int); static void sm_fflush(void); static int sm_fgets(char *, int, FILE *); +static int sm_auth_xoauth2(const char *); #ifdef CYRUS_SASL /* @@ -184,11 +183,13 @@ static int sm_auth_sasl(char *, int, char *, char *); int sm_init (char *client, char *server, char *port, int watch, int verbose, - int debug, int sasl, int saslssf, char *saslmech, char *user, int tls) + int debug, int sasl, int saslssf, char *saslmech, char *user, + const char *xoauth_client_res, int tls) { if (sm_mts == MTS_SMTP) return smtp_init (client, server, port, watch, verbose, - debug, sasl, saslssf, saslmech, user, tls); + debug, sasl, saslssf, saslmech, user, + xoauth_client_res, tls); else return sendmail_init (client, server, watch, verbose, debug, sasl, saslssf, saslmech, user); @@ -197,12 +198,11 @@ sm_init (char *client, char *server, char *port, int watch, int verbose, static int smtp_init (char *client, char *server, char *port, int watch, int verbose, int debug, - int sasl, int saslssf, char *saslmech, char *user, int tls) + int sasl, int saslssf, char *saslmech, char *user, + const char *xoauth_client_res, int tls) { int result, sd1, sd2; -#ifdef CYRUS_SASL - char *server_mechs; -#else /* CYRUS_SASL */ +#ifndef CYRUS_SASL NMH_UNUSED (sasl); NMH_UNUSED (saslssf); NMH_UNUSED (saslmech); @@ -362,6 +362,7 @@ smtp_init (char *client, char *server, char *port, int watch, int verbose, */ if (sasl) { + char *server_mechs; if (! (server_mechs = EHLOset("AUTH"))) { sm_end(NOTOK); return sm_ierror("SMTP server does not support SASL"); @@ -382,6 +383,19 @@ smtp_init (char *client, char *server, char *port, int watch, int verbose, } #endif /* CYRUS_SASL */ + if (xoauth_client_res != NULL) { + char *server_mechs; + if ((server_mechs = EHLOset("AUTH")) == NULL + || stringdex("XOAUTH2", server_mechs) == -1) { + sm_end(NOTOK); + return sm_ierror("SMTP server does not support SASL XOAUTH2"); + } + if (sm_auth_xoauth2(xoauth_client_res) != RP_OK) { + sm_end(NOTOK); + return NOTOK; + } + } + send_options: ; if (watch && EHLOset ("XVRB")) smtalk (SM_HELO, "VERB on"); @@ -1132,6 +1146,36 @@ sm_get_pass(sasl_conn_t *conn, void *context, int id, } #endif /* CYRUS_SASL */ +/* https://developers.google.com/gmail/xoauth2_protocol */ +static int +sm_auth_xoauth2(const char *client_res) +{ + int status = smtalk(SM_AUTH, "AUTH XOAUTH2 %s", client_res); + if (status == 235) { + /* It worked! */ + return RP_OK; + } + + /* + * Status is 334 and sm_reply.text contains base64-encoded JSON. As far as + * epg can tell, no matter the error, the JSON is always the same: + * {"status":"400","schemes":"Bearer","scope":"https://mail.google.com/"} + * I tried these errors: + * - garbage token + * - expired token + * - wrong scope + * - wrong username + */ + /* Then we're supposed to send an empty response ("\r\n"). */ + smtalk(SM_AUTH, ""); + /* + * And now we always get this, again, no matter the error: + * 535-5.7.8 Username and Password not accepted. Learn more at + * 535 5.7.8 http://support.google.com/mail/bin/answer.py?answer=14257 + */ + return RP_BHST; +} + static int sm_ierror (char *fmt, ...) { diff --git a/mts/smtp/smtp.h b/mts/smtp/smtp.h index 72caaccb..268de50a 100644 --- a/mts/smtp/smtp.h +++ b/mts/smtp/smtp.h @@ -16,7 +16,8 @@ struct smtp { * prototypes */ /* int client (); */ -int sm_init (char *, char *, char *, int, int, int, int, int, char *, char *, int); +int sm_init (char *, char *, char *, int, int, int, int, int, char *, char *, + const char *, int); int sm_winit (char *); int sm_wadr (char *, char *, char *); int sm_waend (void); diff --git a/sbr/error.c b/sbr/error.c index 0b6d7777..184c9509 100644 --- a/sbr/error.c +++ b/sbr/error.c @@ -31,7 +31,7 @@ advise (char *what, char *fmt, ...) * print out error message and exit */ void -adios (char *what, char *fmt, ...) +adios (char *what, const char *fmt, ...) { va_list ap; @@ -60,7 +60,7 @@ admonish (char *what, char *fmt, ...) * main routine for printing error messages. */ void -advertise (char *what, char *tail, char *fmt, va_list ap) +advertise (char *what, char *tail, const char *fmt, va_list ap) { int eindex = errno; char buffer[BUFSIZ], err[BUFSIZ]; diff --git a/sbr/oauth.c b/sbr/oauth.c new file mode 100644 index 00000000..6c284d81 --- /dev/null +++ b/sbr/oauth.c @@ -0,0 +1,1158 @@ +/* + * This code is Copyright (c) 2014, by the authors of nmh. See the + * COPYRIGHT file in the root directory of the nmh distribution for + * complete copyright information. + */ + +#include + +#ifdef OAUTH_SUPPORT + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include +#include + +#define JSON_TYPE "application/json" + +/* We pretend access tokens expire 30 seconds earlier than they actually do to + * allow for separate processes to use and refresh access tokens. The process + * that uses the access token (post) has an error if the token is expired; the + * process that refreshes the access token (send) must have already refreshed if + * the expiration is close. + * + * 30s is arbitrary, and hopefully is enough to allow for clock skew. + * Currently only Gmail supports XOAUTH2, and seems to always use a token + * life-time of 3600s, but that is not guaranteed. It is possible for Gmail to + * issue an access token with a life-time so short that even after send + * refreshes it, it's already expired when post tries to use it, but that seems + * unlikely. */ +#define EXPIRY_FUDGE 60 + +/* maximum size for HTTP response bodies + * (not counting header and not null-terminated) */ +#define RESPONSE_BODY_MAX 8192 + +/* Maxium size for URLs and URI-encoded query strings, null-terminated. + * + * Actual maximum we need is based on the size of tokens (limited by + * RESPONSE_BODY_MAX), code user copies from a web page (arbitrarily large), and + * various service parameters (all arbitrarily large). In practice, all these + * are just tens of bytes. It's not hard to change this to realloc as needed, + * but we should still have some limit, so why not this one? + */ +#define URL_MAX 8192 + +struct service_info { + /* Name of service, so we can search static SERVICES (below) and for + * determining default credential file name. */ + char *name; + + /* Human-readable name of the service; in mh_oauth_ctx::svc this is not + * another buffer to free, but a pointer to either static SERVICE data + * (below) or to the name field. */ + char *display_name; + + /* [1] 2.2 Client Identifier, 2.3.1 Client Password */ + char *client_id; + /* [1] 2.3.1 Client Password */ + char *client_secret; + /* [1] 3.1 Authorization Endpoint */ + char *auth_endpoint; + /* [1] 3.1.2 Redirection Endpoint */ + char *redirect_uri; + /* [1] 3.2 Token Endpoint */ + char *token_endpoint; + /* [1] 3.3 Access Token Scope */ + char *scope; +}; + +static const struct service_info SERVICES[] = { + /* https://developers.google.com/accounts/docs/OAuth2InstalledApp */ + { + /* name */ "gmail", + /* display_name */ "Gmail", + + /* client_id */ "91584523849-8lv9kgp1rvp8ahta6fa4b125tn2polcg.apps.googleusercontent.com", + /* client_secret */ "Ua8sX34xyv7hVrKM-U70dKI6", + + /* auth_endpoint */ "https://accounts.google.com/o/oauth2/auth", + /* redirect_uri */ "urn:ietf:wg:oauth:2.0:oob", + /* token_endpoint */ "https://accounts.google.com/o/oauth2/token", + /* scope */ "https://mail.google.com/" + } +}; + +struct mh_oauth_cred { + mh_oauth_ctx *ctx; + + /* opaque access token ([1] 1.4) in null-terminated string */ + char *access_token; + /* opaque refresh token ([1] 1.5) in null-terminated string */ + char *refresh_token; + + /* time at which the access token expires, or 0 if unknown */ + time_t expires_at; + + /* Ignoring token_type ([1] 7.1) because + * https://developers.google.com/accounts/docs/OAuth2InstalledApp says + * "Currently, this field always has the value Bearer". */ +}; + +struct mh_oauth_ctx { + struct service_info svc; + CURL *curl; + FILE *log; + + char buf[URL_MAX]; + + char *cred_fn; + char *sasl_client_res; + char *user_agent; + + mh_oauth_err_code err_code; + + /* If any detailed message about the error is available, this points to it. + * May point to err_buf, or something else. */ + const char *err_details; + + /* Pointer to buffer mh_oauth_err_get_string allocates. */ + char *err_formatted; + + /* Ask libcurl to store errors here. */ + char err_buf[CURL_ERROR_SIZE]; +}; + +struct curl_ctx { + /* inputs */ + + CURL *curl; + /* NULL or a file handle to have curl log diagnostics to */ + FILE *log; + + /* outputs */ + + /* Whether the response was too big; if so, the rest of the output fields + * are undefined. */ + boolean too_big; + + /* HTTP response code */ + long res_code; + + /* NULL or null-terminated value of Content-Type response header field */ + const char *content_type; + + /* number of bytes in the response body */ + size_t res_len; + + /* response body; NOT null-terminated */ + char res_body[RESPONSE_BODY_MAX]; +}; + +static boolean get_json_strings(const char *, size_t, FILE *, ...); +static boolean make_query_url(char *, size_t, CURL *, const char *, ...); +static boolean post(struct curl_ctx *, const char *, const char *); + +char * +mh_oauth_do_xoauth(const char *user, const char *svc, FILE *log) +{ + mh_oauth_ctx *ctx; + mh_oauth_cred *cred; + char *fn; + int failed_to_lock = 0; + FILE *fp; + size_t client_res_len; + char *client_res; + char *client_res_b64; + + if (!mh_oauth_new (&ctx, svc)) adios(NULL, mh_oauth_get_err_string(ctx)); + + if (log != NULL) mh_oauth_log_to(stderr, ctx); + + fn = getcpy(mh_oauth_cred_fn(ctx)); + fp = lkfopendata(fn, "r+", &failed_to_lock); + if (fp == NULL) { + if (errno == ENOENT) { + adios(NULL, "no credentials -- run mhlogin -oauth %s", svc); + } + adios(fn, "failed to open"); + } + if (failed_to_lock) { + adios(fn, "failed to lock"); + } + + if ((cred = mh_oauth_cred_load(fp, ctx)) == NULL) { + adios(NULL, mh_oauth_get_err_string(ctx)); + } + + if (!mh_oauth_access_token_valid(time(NULL), cred)) { + if (!mh_oauth_refresh(cred)) { + if (mh_oauth_get_err_code(ctx) == MH_OAUTH_NO_REFRESH) { + adios(NULL, "no valid credentials -- run mhlogin -oauth %s", + svc); + } + if (mh_oauth_get_err_code(ctx) == MH_OAUTH_BAD_GRANT) { + adios(NULL, "credentials rejected -- run mhlogin -oath %s", + svc); + } + advise(NULL, "error refreshing OAuth2 token"); + adios(NULL, mh_oauth_get_err_string(ctx)); + } + + fseek(fp, 0, SEEK_SET); + if (!mh_oauth_cred_save(fp, cred)) { + adios(NULL, mh_oauth_get_err_string(ctx)); + } + } + + if (lkfclosedata(fp, fn) < 0) { + adios(fn, "failed to close"); + } + free(fn); + + /* XXX writeBase64raw modifies the source buffer! make a copy */ + client_res = getcpy(mh_oauth_sasl_client_response(&client_res_len, user, + cred)); + mh_oauth_cred_free(cred); + mh_oauth_free(ctx); + client_res_b64 = mh_xmalloc(((((client_res_len) + 2) / 3 ) * 4) + 1); + if (writeBase64raw((unsigned char *)client_res, client_res_len, + (unsigned char *)client_res_b64) != OK) { + adios(NULL, "base64 encoding of XOAUTH2 client response failed"); + } + free(client_res); + + return client_res_b64; +} + +static boolean +is_json(const char *content_type) +{ + return content_type != NULL + && strncasecmp(content_type, JSON_TYPE, sizeof JSON_TYPE - 1) == 0; +} + +static void +set_err_details(mh_oauth_ctx *ctx, mh_oauth_err_code code, const char *details) +{ + ctx->err_code = code; + ctx->err_details = details; +} + +static void +set_err(mh_oauth_ctx *ctx, mh_oauth_err_code code) +{ + set_err_details(ctx, code, NULL); +} + +static void +set_err_http(mh_oauth_ctx *ctx, const struct curl_ctx *curl_ctx) +{ + char *error = NULL; + mh_oauth_err_code code; + /* 5.2. Error Response says error response should use status code 400 and + * application/json body. If Content-Type matches, try to parse the body + * regardless of the status code. */ + if (curl_ctx->res_body != NULL + && is_json(curl_ctx->content_type) + && get_json_strings(curl_ctx->res_body, curl_ctx->res_len, ctx->log, + "error", &error, (void *)NULL) + && error != NULL) { + if (strcmp(error, "invalid_grant") == 0) { + code = MH_OAUTH_BAD_GRANT; + } else { + /* All other errors indicate a bug, not anything the user did. */ + code = MH_OAUTH_REQUEST_BAD; + } + } else { + code = MH_OAUTH_RESPONSE_BAD; + } + set_err(ctx, code); + free(error); +} + +/* Copy service info so we don't have to free it only sometimes. */ +static void +copy_svc(struct service_info *to, const struct service_info *from) +{ + to->display_name = from->display_name; +#define copy(_field_) to->_field_ = getcpy(from->_field_) + copy(name); + copy(scope); + copy(client_id); + copy(client_secret); + copy(auth_endpoint); + copy(token_endpoint); + copy(redirect_uri); +#undef copy +} + +/* Return profile component node name for a service parameter. */ +static char * +node_name_for_svc(const char *base_name, const char *svc) +{ + char *result = mh_xmalloc(sizeof "oauth-" - 1 + + strlen(svc) + + 1 /* '-' */ + + strlen(base_name) + + 1 /* '\0' */); + sprintf(result, "oauth-%s-%s", svc, base_name); + /* TODO: s/_/-/g ? */ + return result; +} + +/* Update one service_info field if overridden in profile. */ +static void +update_svc_field(char **field, const char *base_name, const char *svc) +{ + char *name = node_name_for_svc(base_name, svc); + const char *value = context_find(name); + if (value != NULL) { + free(*field); + *field = getcpy(value); + } + free(name); +} + +/* Update all service_info fields that are overridden in profile. */ +static boolean +update_svc(struct service_info *svc, const char *svc_name, mh_oauth_ctx *ctx) +{ +#define update(name) \ + update_svc_field(&svc->name, #name, svc_name); \ + if (svc->name == NULL) { \ + set_err_details(ctx, MH_OAUTH_BAD_PROFILE, #name " is missing"); \ + return FALSE; \ + } + update(scope); + update(client_id); + update(client_secret); + update(auth_endpoint); + update(token_endpoint); + update(redirect_uri); +#undef update + + if (svc->name == NULL) { + svc->name = getcpy(svc_name); + } + + if (svc->display_name == NULL) { + svc->display_name = svc->name; + } + + return TRUE; +} + +static char * +make_user_agent() +{ + const char *curl = curl_version_info(CURLVERSION_NOW)->version; + char *s = mh_xmalloc(strlen(user_agent) + + 1 + + sizeof "libcurl" + + 1 + + strlen(curl) + + 1); + sprintf(s, "%s libcurl/%s", user_agent, curl); + return s; +} + +boolean +mh_oauth_new(mh_oauth_ctx **result, const char *svc_name) +{ + mh_oauth_ctx *ctx = *result = mh_xmalloc(sizeof *ctx); + size_t i; + + ctx->curl = NULL; + + ctx->log = NULL; + ctx->cred_fn = ctx->sasl_client_res = ctx->err_formatted = NULL; + + ctx->svc.name = ctx->svc.display_name = NULL; + ctx->svc.scope = ctx->svc.client_id = NULL; + ctx->svc.client_secret = ctx->svc.auth_endpoint = NULL; + ctx->svc.token_endpoint = ctx->svc.redirect_uri = NULL; + + for (i = 0; i < sizeof SERVICES / sizeof SERVICES[0]; i++) { + if (strcmp(SERVICES[i].name, svc_name) == 0) { + copy_svc(&ctx->svc, &SERVICES[i]); + break; + } + } + + if (!update_svc(&ctx->svc, svc_name, ctx)) { + return FALSE; + } + + ctx->curl = curl_easy_init(); + if (ctx->curl == NULL) { + set_err(ctx, MH_OAUTH_CURL_INIT); + return FALSE; + } + curl_easy_setopt(ctx->curl, CURLOPT_ERRORBUFFER, ctx->err_buf); + + ctx->user_agent = make_user_agent(); + + if (curl_easy_setopt(ctx->curl, CURLOPT_USERAGENT, + ctx->user_agent) != CURLE_OK) { + set_err_details(ctx, MH_OAUTH_CURL_INIT, ctx->err_buf); + return FALSE; + } + + return TRUE; +} + +void +mh_oauth_free(mh_oauth_ctx *ctx) +{ + free(ctx->svc.name); + free(ctx->svc.scope); + free(ctx->svc.client_id); + free(ctx->svc.client_secret); + free(ctx->svc.auth_endpoint); + free(ctx->svc.token_endpoint); + free(ctx->svc.redirect_uri); + free(ctx->cred_fn); + free(ctx->sasl_client_res); + free(ctx->err_formatted); + free(ctx->user_agent); + + if (ctx->curl != NULL) { + curl_easy_cleanup(ctx->curl); + } + free(ctx); +} + +const char * +mh_oauth_svc_display_name(const mh_oauth_ctx *ctx) +{ + return ctx->svc.display_name; +} + +void +mh_oauth_log_to(FILE *log, mh_oauth_ctx *ctx) +{ + ctx->log = log; +} + +mh_oauth_err_code +mh_oauth_get_err_code(const mh_oauth_ctx *ctx) +{ + return ctx->err_code; +} + +const char * +mh_oauth_get_err_string(mh_oauth_ctx *ctx) +{ + char *result; + const char *base; + + free(ctx->err_formatted); + + switch (ctx->err_code) { + case MH_OAUTH_BAD_PROFILE: + base = "incomplete OAuth2 service definition"; + break; + case MH_OAUTH_CURL_INIT: + base = "error initializing libcurl"; + break; + case MH_OAUTH_REQUEST_INIT: + base = "local error initializing HTTP request"; + break; + case MH_OAUTH_POST: + base = "error making HTTP request to OAuth2 authorization endpoint"; + break; + case MH_OAUTH_RESPONSE_TOO_BIG: + base = "refusing to process response body larger than 8192 bytes"; + break; + case MH_OAUTH_RESPONSE_BAD: + base = "invalid response"; + break; + case MH_OAUTH_BAD_GRANT: + base = "bad grant (authorization code or refresh token)"; + break; + case MH_OAUTH_REQUEST_BAD: + base = "bad OAuth request; re-run with -snoop and send REDACTED output" + " to nmh-workers"; + break; + case MH_OAUTH_NO_REFRESH: + base = "no refresh token"; + break; + case MH_OAUTH_CRED_FILE: + base = "error loading cred file"; + break; + default: + base = "unknown error"; + } + if (ctx->err_details == NULL) { + return ctx->err_formatted = getcpy(base); + } + /* length of the two strings plus ": " and '\0' */ + result = mh_xmalloc(strlen(base) + strlen(ctx->err_details) + 3); + sprintf(result, "%s: %s", base, ctx->err_details); + return ctx->err_formatted = result; +} + +const char * +mh_oauth_get_authorize_url(mh_oauth_ctx *ctx) +{ + /* [1] 4.1.1 Authorization Request */ + if (!make_query_url(ctx->buf, sizeof ctx->buf, ctx->curl, + ctx->svc.auth_endpoint, + "response_type", "code", + "client_id", ctx->svc.client_id, + "redirect_uri", ctx->svc.redirect_uri, + "scope", ctx->svc.scope, + (void *)NULL)) { + set_err(ctx, MH_OAUTH_REQUEST_INIT); + return NULL; + } + return ctx->buf; +} + +static boolean +cred_from_response(mh_oauth_cred *cred, const char *content_type, + const char *input, size_t input_len) +{ + boolean result = FALSE; + char *access_token, *expires_in, *refresh_token; + const mh_oauth_ctx *ctx = cred->ctx; + + if (!is_json(content_type)) { + return FALSE; + } + + access_token = expires_in = refresh_token = NULL; + if (!get_json_strings(input, input_len, ctx->log, + "access_token", &access_token, + "expires_in", &expires_in, + "refresh_token", &refresh_token, + (void *)NULL)) { + goto out; + } + + if (access_token == NULL) { + /* Response is invalid, but if it has a refresh token, we can try. */ + if (refresh_token == NULL) { + goto out; + } + } + + result = TRUE; + + free(cred->access_token); + cred->access_token = access_token; + access_token = NULL; + + cred->expires_at = 0; + if (expires_in != NULL) { + long e; + errno = 0; + e = strtol(expires_in, NULL, 10); + if (errno == 0) { + if (e > 0) { + cred->expires_at = time(NULL) + e; + } + } else if (ctx->log != NULL) { + fprintf(ctx->log, "* invalid expiration: %s\n", expires_in); + } + } + + /* [1] 6 Refreshing an Access Token says a new refresh token may be issued + * in refresh responses. */ + if (refresh_token != NULL) { + free(cred->refresh_token); + cred->refresh_token = refresh_token; + refresh_token = NULL; + } + + out: + free(refresh_token); + free(expires_in); + free(access_token); + return result; +} + +static boolean +do_access_request(mh_oauth_cred *cred, const char *req_body) +{ + mh_oauth_ctx *ctx = cred->ctx; + struct curl_ctx curl_ctx; + + curl_ctx.curl = ctx->curl; + curl_ctx.log = ctx->log; + if (!post(&curl_ctx, ctx->svc.token_endpoint, req_body)) { + if (curl_ctx.too_big) { + set_err(ctx, MH_OAUTH_RESPONSE_TOO_BIG); + } else { + set_err_details(ctx, MH_OAUTH_POST, ctx->err_buf); + } + return FALSE; + } + + if (curl_ctx.res_code != 200) { + set_err_http(ctx, &curl_ctx); + return FALSE; + } + + if (!cred_from_response(cred, curl_ctx.content_type, curl_ctx.res_body, + curl_ctx.res_len)) { + set_err(ctx, MH_OAUTH_RESPONSE_BAD); + return FALSE; + } + + return TRUE; +} + +mh_oauth_cred * +mh_oauth_authorize(const char *code, mh_oauth_ctx *ctx) +{ + mh_oauth_cred *result; + + if (!make_query_url(ctx->buf, sizeof ctx->buf, ctx->curl, NULL, + "code", code, + "grant_type", "authorization_code", + "redirect_uri", ctx->svc.redirect_uri, + "client_id", ctx->svc.client_id, + "client_secret", ctx->svc.client_secret, + (void *)NULL)) { + set_err(ctx, MH_OAUTH_REQUEST_INIT); + return NULL; + } + + result = mh_xmalloc(sizeof *result); + result->ctx = ctx; + result->access_token = result->refresh_token = NULL; + + if (!do_access_request(result, ctx->buf)) { + free(result); + return NULL; + } + + return result; +} + +boolean +mh_oauth_refresh(mh_oauth_cred *cred) +{ + boolean result; + mh_oauth_ctx *ctx = cred->ctx; + + if (cred->refresh_token == NULL) { + set_err(ctx, MH_OAUTH_NO_REFRESH); + return FALSE; + } + + if (!make_query_url(ctx->buf, sizeof ctx->buf, ctx->curl, NULL, + "grant_type", "refresh_token", + "refresh_token", cred->refresh_token, + "client_id", ctx->svc.client_id, + "client_secret", ctx->svc.client_secret, + (void *)NULL)) { + set_err(ctx, MH_OAUTH_REQUEST_INIT); + return FALSE; + } + + result = do_access_request(cred, ctx->buf); + + if (result && cred->access_token == NULL) { + set_err_details(ctx, MH_OAUTH_RESPONSE_BAD, "no access token"); + return FALSE; + } + + return result; +} + +boolean +mh_oauth_access_token_valid(time_t t, const mh_oauth_cred *cred) +{ + return cred->access_token != NULL && t + EXPIRY_FUDGE < cred->expires_at; +} + +void +mh_oauth_cred_free(mh_oauth_cred *cred) +{ + free(cred->refresh_token); + free(cred->access_token); + free(cred); +} + +const char * +mh_oauth_cred_fn(mh_oauth_ctx *ctx) +{ + char *result, *result_if_allocated; + const char *svc = ctx->svc.name; + + char *component = node_name_for_svc("credential-file", svc); + result = context_find(component); + free(component); + + if (result == NULL) { + result = mh_xmalloc(sizeof "oauth-" - 1 + + strlen(svc) + + 1 /* '\0' */); + sprintf(result, "oauth-%s", svc); + result_if_allocated = result; + } else { + result_if_allocated = NULL; + } + + if (result[0] != '/') { + const char *tmp = m_maildir(result); + free(result_if_allocated); + result = getcpy(tmp); + } + + free(ctx->cred_fn); + return ctx->cred_fn = result; +} + +boolean +mh_oauth_cred_save(FILE *fp, mh_oauth_cred *cred) +{ + int fd = fileno(fp); + if (fchmod(fd, S_IRUSR | S_IWUSR) < 0) goto err; + if (ftruncate(fd, 0) < 0) goto err; + if (cred->access_token != NULL) { + if (fprintf(fp, "access: %s\n", cred->access_token) < 0) goto err; + } + if (cred->refresh_token != NULL) { + if (fprintf(fp, "refresh: %s\n", cred->refresh_token) < 0) goto err; + } + if (cred->expires_at > 0) { + if (fprintf(fp, "expire: %ld\n", (long)cred->expires_at) < 0) goto err; + } + return TRUE; + + err: + set_err(cred->ctx, MH_OAUTH_CRED_FILE); + return FALSE; +} + +static boolean +parse_cred(char **access, char **refresh, char **expire, FILE *fp, + mh_oauth_ctx *ctx) +{ + boolean result = FALSE; + char name[NAMESZ], value_buf[BUFSIZ]; + int state; + m_getfld_state_t getfld_ctx = 0; + + for (;;) { + int size = sizeof value_buf; + switch (state = m_getfld(&getfld_ctx, name, value_buf, &size, fp)) { + case FLD: + case FLDPLUS: { + char **save; + if (strcmp(name, "access") == 0) { + save = access; + } else if (strcmp(name, "refresh") == 0) { + save = refresh; + } else if (strcmp(name, "expire") == 0) { + save = expire; + } else { + set_err_details(ctx, MH_OAUTH_CRED_FILE, "unexpected field"); + break; + } + + if (state == FLD) { + *save = trimcpy(value_buf); + } else { + char *tmp = getcpy(value_buf); + while (state == FLDPLUS) { + size = sizeof value_buf; + state = m_getfld(&getfld_ctx, name, value_buf, &size, fp); + tmp = add(value_buf, tmp); + } + *save = trimcpy(tmp); + free(tmp); + } + continue; + } + + case BODY: + case FILEEOF: + result = TRUE; + break; + + default: + /* Not adding details for LENERR/FMTERR because m_getfld already + * wrote advise message to stderr. */ + set_err(ctx, MH_OAUTH_CRED_FILE); + break; + } + break; + } + m_getfld_state_destroy(&getfld_ctx); + return result; +} + +mh_oauth_cred * +mh_oauth_cred_load(FILE *fp, mh_oauth_ctx *ctx) +{ + mh_oauth_cred *result; + time_t expires_at = 0; + char *access, *refresh, *expire; + + access = refresh = expire = NULL; + if (!parse_cred(&access, &refresh, &expire, fp, ctx)) { + free(access); + free(refresh); + free(expire); + return NULL; + } + + if (expire != NULL) { + errno = 0; + expires_at = strtol(expire, NULL, 10); + free(expire); + if (errno != 0) { + set_err_details(ctx, MH_OAUTH_CRED_FILE, "invalid expiration time"); + free(access); + free(refresh); + return NULL; + } + } + + result = mh_xmalloc(sizeof *result); + result->ctx = ctx; + result->access_token = access; + result->refresh_token = refresh; + result->expires_at = expires_at; + + return result; +} + +const char * +mh_oauth_sasl_client_response(size_t *res_len, + const char *user, const mh_oauth_cred *cred) +{ + size_t len = sizeof "user=" - 1 + + strlen(user) + + sizeof "\1auth=Bearer " - 1 + + strlen(cred->access_token) + + sizeof "\1\1" - 1; + free(cred->ctx->sasl_client_res); + cred->ctx->sasl_client_res = mh_xmalloc(len + 1); + *res_len = len; + sprintf(cred->ctx->sasl_client_res, "user=%s\1auth=Bearer %s\1\1", + user, cred->access_token); + return cred->ctx->sasl_client_res; +} + +/******************************************************************************* + * building URLs and making HTTP requests with libcurl + */ + +/* + * Build null-terminated URL in the array pointed to by s. If the URL doesn't + * fit within size (including the terminating null byte), return FALSE without * + * building the entire URL. Some of URL may already have been written into the + * result array in that case. + */ +static boolean +make_query_url(char *s, size_t size, CURL *curl, const char *base_url, ...) +{ + boolean result = FALSE; + size_t len; + char *prefix; + va_list ap; + const char *name; + + if (base_url == NULL) { + len = 0; + prefix = ""; + } else { + len = sprintf(s, "%s", base_url); + prefix = "?"; + } + + va_start(ap, base_url); + for (name = va_arg(ap, char *); name != NULL; name = va_arg(ap, char *)) { + char *name_esc = curl_easy_escape(curl, name, 0); + char *val_esc = curl_easy_escape(curl, va_arg(ap, char *), 0); + /* prefix + name_esc + '=' + val_esc + '\0' must fit within size */ + size_t new_len = len + + strlen(prefix) + + strlen(name_esc) + + 1 /* '=' */ + + strlen(val_esc); + if (new_len + 1 > size) { + free(name_esc); + free(val_esc); + goto out; + } + sprintf(s + len, "%s%s=%s", prefix, name_esc, val_esc); + free(name_esc); + free(val_esc); + len = new_len; + prefix = "&"; + } + + result = TRUE; + + out: + va_end(ap); + return result; +} + +static int +debug_callback(const CURL *handle, curl_infotype type, const char *data, + size_t size, void *userptr) +{ + FILE *fp = userptr; + NMH_UNUSED(handle); + + switch (type) { + case CURLINFO_HEADER_IN: + case CURLINFO_DATA_IN: + fputs("< ", fp); + break; + case CURLINFO_HEADER_OUT: + case CURLINFO_DATA_OUT: + fputs("> ", fp); + break; + default: + return 0; + } + fwrite(data, 1, size, fp); + if (data[size - 1] != '\n') { + fputs("\n", fp); + } + fflush(fp); + return 0; +} + +static size_t +write_callback(const char *ptr, size_t size, size_t nmemb, void *userdata) +{ + struct curl_ctx *ctx = userdata; + size_t new_len; + + if (ctx->too_big) { + return 0; + } + + size *= nmemb; + new_len = ctx->res_len + size; + if (new_len > sizeof ctx->res_body) { + ctx->too_big = TRUE; + return 0; + } + + memcpy(ctx->res_body + ctx->res_len, ptr, size); + ctx->res_len = new_len; + + return size; +} + +static boolean +post(struct curl_ctx *ctx, const char *url, const char *req_body) +{ + CURL *curl = ctx->curl; + CURLcode status; + + ctx->too_big = FALSE; + ctx->res_len = 0; + + if (ctx->log != NULL) { + curl_easy_setopt(curl, CURLOPT_VERBOSE, (long)1); + curl_easy_setopt(curl, CURLOPT_DEBUGFUNCTION, debug_callback); + curl_easy_setopt(curl, CURLOPT_DEBUGDATA, ctx->log); + } + + if ((status = curl_easy_setopt(curl, CURLOPT_URL, url)) != CURLE_OK) { + return FALSE; + } + + curl_easy_setopt(curl, CURLOPT_POSTFIELDS, req_body); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, ctx); + + status = curl_easy_perform(curl); + /* first check for error from callback */ + if (ctx->too_big) { + return FALSE; + } + /* now from curl */ + if (status != CURLE_OK) { + return FALSE; + } + + if ((status = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, + &ctx->res_code)) != CURLE_OK + || (status = curl_easy_getinfo(curl, CURLINFO_CONTENT_TYPE, + &ctx->content_type)) != CURLE_OK) { + return FALSE; + } + + return TRUE; +} + +/******************************************************************************* + * JSON processing + */ + +/* We need 2 for each key/value pair plus 1 for the enclosing object, which + * means we only need 9 for Gmail. Clients must not fail if the server returns + * more, though, e.g. for protocol extensions. */ +#define JSMN_TOKENS 16 + +/* + * Parse JSON, store pointer to array of jsmntok_t in tokens. + * + * Returns whether parsing is successful. + * + * Even in that case, tokens has been allocated and must be freed. + */ +static boolean +parse_json(jsmntok_t **tokens, size_t *tokens_len, + const char *input, size_t input_len, FILE *log) +{ + jsmn_parser p; + jsmnerr_t r; + + *tokens_len = JSMN_TOKENS; + *tokens = mh_xmalloc(*tokens_len * sizeof **tokens); + + jsmn_init(&p); + while ((r = jsmn_parse(&p, input, input_len, + *tokens, *tokens_len)) == JSMN_ERROR_NOMEM) { + *tokens_len = 2 * *tokens_len; + if (log != NULL) { + fprintf(log, "* need more jsmntok_t! allocating %ld\n", + (long)*tokens_len); + } + /* Don't need to limit how much we allocate; we already limited the size + of the response body. */ + *tokens = mh_xrealloc(*tokens, *tokens_len * sizeof **tokens); + } + if (r == 0) { + return FALSE; + } + + return TRUE; +} + +/* + * Search input and tokens for the value identified by null-terminated name. + * + * If found, allocate a null-terminated copy of the value and store the address + * in val. val is left untouched if not found. + */ +static void +get_json_string(char **val, const char *input, const jsmntok_t *tokens, + const char *name) +{ + /* number of top-level tokens (not counting object/list children) */ + int token_count = tokens[0].size * 2; + /* number of tokens to skip when we encounter objects and lists */ + /* We only look for top-level strings. */ + int skip_tokens = 0; + /* whether the current token represents a field name */ + /* The next token will be the value. */ + boolean is_key = TRUE; + + int i; + for (i = 1; i <= token_count; i++) { + const char *key; + int key_len; + if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) { + /* We're not interested in any array or object children; skip. */ + int children = tokens[i].size; + if (tokens[i].type == JSMN_OBJECT) { + /* Object size counts key/value pairs, skip both. */ + children *= 2; + } + /* Add children to token_count. */ + token_count += children; + if (skip_tokens == 0) { + /* This token not already skipped; skip it. */ + /* Would already be skipped if child of object or list. */ + skip_tokens++; + } + /* Skip this token's children. */ + skip_tokens += children; + } + if (skip_tokens > 0) { + skip_tokens--; + /* When we finish with the object or list, we'll have a key. */ + is_key = TRUE; + continue; + } + if (is_key) { + is_key = FALSE; + continue; + } + key = input + tokens[i - 1].start; + key_len = tokens[i - 1].end - tokens[i - 1].start; + if (strncmp(key, name, key_len) == 0) { + int val_len = tokens[i].end - tokens[i].start; + *val = mh_xmalloc(val_len + 1); + memcpy(*val, input + tokens[i].start, val_len); + (*val)[val_len] = '\0'; + return; + } + is_key = TRUE; + } +} + +/* + * Parse input as JSON, extracting specified string values. + * + * Variadic arguments are pairs of null-terminated strings indicating the value + * to extract from the JSON and addresses into which pointers to null-terminated + * copies of the values are written. These must be followed by one NULL pointer + * to indicate the end of pairs. + * + * The extracted strings are copies which caller must free. If any name is not + * found, the address to store the value is not touched. + * + * Returns non-zero if parsing is successful. + * + * When parsing failed, no strings have been copied. + * + * log may be used for debug-logging if not NULL. + */ +static boolean +get_json_strings(const char *input, size_t input_len, FILE *log, ...) +{ + boolean result = FALSE; + jsmntok_t *tokens; + size_t tokens_len; + va_list ap; + const char *name; + + if (!parse_json(&tokens, &tokens_len, input, input_len, log)) { + goto out; + } + + if (tokens->type != JSMN_OBJECT || tokens->size == 0) { + goto out; + } + + result = TRUE; + + va_start(ap, log); + for (name = va_arg(ap, char *); name != NULL; name = va_arg(ap, char *)) { + get_json_string(va_arg(ap, char **), input, tokens, name); + } + + out: + va_end(ap); + free(tokens); + return result; +} + +#endif diff --git a/sbr/readconfig.c b/sbr/readconfig.c index 2e319fe6..5d116893 100644 --- a/sbr/readconfig.c +++ b/sbr/readconfig.c @@ -42,7 +42,7 @@ static struct node **opp = NULL; void -readconfig (struct node **npp, FILE *ib, char *file, int ctx) +readconfig (struct node **npp, FILE *ib, const char *file, int ctx) { register int state; register char *cp; diff --git a/test/fakehttp.c b/test/fakehttp.c new file mode 100644 index 00000000..f773a73d --- /dev/null +++ b/test/fakehttp.c @@ -0,0 +1,126 @@ +/* + * fakehttp - A fake HTTP server used by the nmh test suite + * + * This code is Copyright (c) 2014, by the authors of nmh. See the + * COPYRIGHT file in the root directory of the nmh distribution for + * complete copyright information. + */ + +#include +#include +#include +#include +#include +#include +#include +#include + +#define LINESIZE 1024 +#define PIDFN "/tmp/fakehttp.pid" + +int serve(const char *, const char *); +void putcrlf(int, char *); + +static void +save_req(int conn, FILE *req) +{ + char buf[BUFSIZ]; + ssize_t r; + int e; /* used to save errno */ + int started = 0; /* whether the request has started coming in */ + + if (fcntl(conn, F_SETFL, O_NONBLOCK) < 0) { + fprintf(stderr, "Unable to make socket non-blocking: %s\n", + strerror(errno)); + exit(1); + } + + for (;;) { + r = read(conn, buf, sizeof buf); + if (!started) { + /* First keep trying until some data is ready; for testing, don't + * bother with using select to wait for input. */ + if (r < 0) { + e = errno; + if (e == EAGAIN || e == EWOULDBLOCK) { + continue; /* keep waiting */ + } + fclose(req); + fprintf(stderr, "Unable to read socket: %s\n", strerror(e)); + exit(1); + } + /* Request is here. Fall through to the fwrite below and keep + * reading. */ + started = 1; + } + if (r < 0) { + e = errno; + fputs("\n", req); /* req body usually has no newline */ + fclose(req); + if (e != EAGAIN && e != EWOULDBLOCK) { + fprintf(stderr, "Unable to read socket: %s\n", strerror(e)); + exit(1); + } + /* For testing, we can get away without understand the HTTP request + * and just treating the would-block case as meaning the request is + * all done. */ + return; + } + /* make tests simpler by eliding carriage-returns? */ + fwrite(buf, 1, r, req); + } +} + +static void +send_res(int conn, FILE *res) +{ + size_t size; + ssize_t len; + char *res_line = NULL; + + while ((len = getline(&res_line, &size, res)) > 0) { + res_line[len - 1] = '\0'; + putcrlf(conn, res_line); + } + free(res_line); + if (!feof(res)) { + fprintf(stderr, "read response failed: %s\n", strerror(errno)); + exit(1); + } +} + +int +main(int argc, char *argv[]) +{ + struct st; + int conn; + FILE *req, *res; + + if (argc != 4) { + fprintf(stderr, "Usage: %s output-filename port response\n", + argv[0]); + exit(1); + } + + if (!(req = fopen(argv[1], "w"))) { + fprintf(stderr, "Unable to open output file \"%s\": %s\n", + argv[1], strerror(errno)); + exit(1); + } + + if (!(res = fopen(argv[3], "r"))) { + fprintf(stderr, "Unable to open response \"%s\": %s\n", + argv[3], strerror(errno)); + exit(1); + } + + conn = serve(PIDFN, argv[2]); + + save_req(conn, req); + + send_res(conn, res); + + close(conn); + + return 0; +} diff --git a/test/fakepop.c b/test/fakepop.c index 4d023545..bc2baae0 100644 --- a/test/fakepop.c +++ b/test/fakepop.c @@ -10,36 +10,28 @@ #include #include #include -#include #include -#include -#include #include -#include -#include -#include #include -#include #define PIDFILE "/tmp/fakepop.pid" #define LINESIZE 1024 #define BUFALLOC 4096 #define CHECKUSER() if (!user) { \ - putpop(s, "-ERR Aren't you forgetting " \ + putcrlf(s, "-ERR Aren't you forgetting " \ "something? Like the USER command?"); \ continue; \ } -#define CHECKUSERPASS() CHECKUSER() \ - if (! pass) { \ - putpop(s, "-ERR Um, hello? Forget to " \ +#define CHECKAUTH() if (!auth) { \ + putcrlf(s, "-ERR Um, hello? Forget to " \ "log in?"); \ continue; \ } -static void killpidfile(void); -static void handleterm(int); -static void putpop(int, char *); +void putcrlf(int, char *); +int serve(const char *, const char *); + static void putpopbulk(int, char *); static int getpop(int, char *, ssize_t); static char *readmessage(FILE *); @@ -47,16 +39,12 @@ static char *readmessage(FILE *); int main(int argc, char *argv[]) { - struct addrinfo hints, *res; - struct stat st; - FILE **mfiles, *pid; + FILE **mfiles; char line[LINESIZE]; - fd_set readfd; - struct timeval tv; - pid_t child; - int rc, l, s, on, user = 0, pass = 0, i, j; + int rc, s, user = 0, auth = 0, i, j; int numfiles; size_t *octets; + const char *xoauth; if (argc < 5) { fprintf(stderr, "Usage: %s port username " @@ -64,6 +52,12 @@ main(int argc, char *argv[]) exit(1); } + if (strcmp(argv[2], "XOAUTH") == 0) { + xoauth = argv[3]; + } else { + xoauth = NULL; + } + numfiles = argc - 4; mfiles = malloc(sizeof(FILE *) * numfiles); @@ -105,153 +99,13 @@ main(int argc, char *argv[]) rewind(mfiles[j]); } - /* - * If there is a pid file around, kill the previously running - * fakepop process. - */ - - if (stat(PIDFILE, &st) == 0) { - long oldpid; - - if (!(pid = fopen(PIDFILE, "r"))) { - fprintf(stderr, "Cannot open " PIDFILE - " (%s), continuing ...\n", strerror(errno)); - } else { - rc = fscanf(pid, "%ld", &oldpid); - fclose(pid); - - if (rc != 1) { - fprintf(stderr, "Unable to parse pid in " - PIDFILE ", continuing ...\n"); - } else { - kill((pid_t) oldpid, SIGTERM); - } - } - - unlink(PIDFILE); - } - - memset(&hints, 0, sizeof(hints)); - - hints.ai_family = PF_INET; - hints.ai_socktype = SOCK_STREAM; - hints.ai_protocol = IPPROTO_TCP; - hints.ai_flags = AI_PASSIVE; - - rc = getaddrinfo("127.0.0.1", argv[1], &hints, &res); - - if (rc) { - fprintf(stderr, "Unable to resolve localhost/%s: %s\n", - argv[1], gai_strerror(rc)); - exit(1); - } - - l = socket(res->ai_family, res->ai_socktype, res->ai_protocol); - - if (l == -1) { - fprintf(stderr, "Unable to create listening socket: %s\n", - strerror(errno)); - exit(1); - } - - on = 1; - - if (setsockopt(l, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1) { - fprintf(stderr, "Unable to set SO_REUSEADDR: %s\n", - strerror(errno)); - exit(1); - } - - if (bind(l, res->ai_addr, res->ai_addrlen) == -1) { - fprintf(stderr, "Unable to bind socket: %s\n", strerror(errno)); - exit(1); - } - - if (listen(l, 1) == -1) { - fprintf(stderr, "Unable to listen on socket: %s\n", - strerror(errno)); - exit(1); - } - - /* - * Fork off a copy of ourselves, print out our child pid, then - * exit. - */ - - switch (child = fork()) { - case -1: - fprintf(stderr, "Unable to fork child: %s\n", strerror(errno)); - exit(1); - break; - case 0: - /* - * Close stdin and stdout so $() in the shell will get an - * EOF. For now leave stderr open. - */ - fclose(stdin); - fclose(stdout); - break; - default: - printf("%ld\n", (long) child); - exit(0); - } - - /* - * Now that our socket and files are set up, wait 30 seconds for - * a connection. If there isn't one, then exit. - */ - - if (!(pid = fopen(PIDFILE, "w"))) { - fprintf(stderr, "Cannot open " PIDFILE ": %s\n", - strerror(errno)); - exit(1); - } - - fprintf(pid, "%ld\n", (long) getpid()); - fclose(pid); - - signal(SIGTERM, handleterm); - atexit(killpidfile); - - FD_ZERO(&readfd); - FD_SET(l, &readfd); - - tv.tv_sec = 30; - tv.tv_usec = 0; - - rc = select(l + 1, &readfd, NULL, NULL, &tv); - - if (rc < 0) { - fprintf(stderr, "select() failed: %s\n", strerror(errno)); - exit(1); - } - - /* - * If we get a timeout, just silently exit - */ - - if (rc == 0) { - exit(1); - } - - /* - * We got a connection; accept it. Right after that close our - * listening socket so we won't get any more connections on it. - */ - - if ((s = accept(l, NULL, NULL)) == -1) { - fprintf(stderr, "Unable to accept connection: %s\n", - strerror(errno)); - exit(1); - } - - close(l); + s = serve(PIDFILE, argv[1]); /* * Pretend to be a POP server */ - putpop(s, "+OK Not really a POP server, but we play one on TV"); + putcrlf(s, "+OK Not really a POP server, but we play one on TV"); for (;;) { char linebuf[LINESIZE]; @@ -263,26 +117,42 @@ main(int argc, char *argv[]) if (strcasecmp(linebuf, "CAPA") == 0) { putpopbulk(s, "+OK We have no capabilities, really\r\n" - "FAKE-CAPABILITY\r\n.\r\n"); + "FAKE-CAPABILITY\r\n"); + if (xoauth != NULL) { + putcrlf(s, "SASL XOAUTH2"); + } + putcrlf(s, "."); } else if (strncasecmp(linebuf, "USER ", 5) == 0) { if (strcmp(linebuf + 5, argv[2]) == 0) { - putpop(s, "+OK Niiiice!"); + putcrlf(s, "+OK Niiiice!"); user = 1; } else { - putpop(s, "-ERR Don't play me, bro!"); + putcrlf(s, "-ERR Don't play me, bro!"); } } else if (strncasecmp(linebuf, "PASS ", 5) == 0) { CHECKUSER(); if (strcmp(linebuf + 5, argv[3]) == 0) { - putpop(s, "+OK Aren't you a sight " + putcrlf(s, "+OK Aren't you a sight " "for sore eyes!"); - pass = 1; + auth = 1; } else { - putpop(s, "-ERR C'mon!"); + putcrlf(s, "-ERR C'mon!"); } + } else if (xoauth != NULL + && strncasecmp(linebuf, "AUTH XOAUTH2", 12) == 0) { + if (strstr(linebuf, xoauth) == NULL) { + putcrlf(s, "+ base64-json-err"); + rc = getpop(s, linebuf, sizeof(linebuf)); + if (rc != 0) + break; /* Error or EOF */ + putcrlf(s, "-ERR [AUTH] Invalid credentials."); + continue; + } + putcrlf(s, "+OK Welcome."); + auth = 1; } else if (strcasecmp(linebuf, "STAT") == 0) { size_t total = 0; - CHECKUSERPASS(); + CHECKAUTH(); for (i = 0, j = 0; i < numfiles; i++) { if (mfiles[i]) { total += octets[i]; @@ -291,78 +161,59 @@ main(int argc, char *argv[]) } snprintf(linebuf, sizeof(linebuf), "+OK %d %d", i, (int) total); - putpop(s, linebuf); + putcrlf(s, linebuf); } else if (strncasecmp(linebuf, "RETR ", 5) == 0) { - CHECKUSERPASS(); + CHECKAUTH(); rc = sscanf(linebuf + 5, "%d", &i); if (rc != 1) { - putpop(s, "-ERR Whaaaa...?"); + putcrlf(s, "-ERR Whaaaa...?"); continue; } if (i < 1 || i > numfiles) { - putpop(s, "-ERR That message number is " + putcrlf(s, "-ERR That message number is " "out of range, jerkface!"); continue; } if (mfiles[i - 1] == NULL) { - putpop(s, "-ERR Sorry, don't have it anymore"); + putcrlf(s, "-ERR Sorry, don't have it anymore"); } else { char *buf = readmessage(mfiles[i - 1]); - putpop(s, "+OK Here you go ..."); + putcrlf(s, "+OK Here you go ..."); putpopbulk(s, buf); free(buf); } } else if (strncasecmp(linebuf, "DELE ", 5) == 0) { - CHECKUSERPASS(); + CHECKAUTH(); rc = sscanf(linebuf + 5, "%d", &i); if (rc != 1) { - putpop(s, "-ERR Whaaaa...?"); + putcrlf(s, "-ERR Whaaaa...?"); continue; } if (i < 1 || i > numfiles) { - putpop(s, "-ERR That message number is " + putcrlf(s, "-ERR That message number is " "out of range, jerkface!"); continue; } if (mfiles[i - 1] == NULL) { - putpop(s, "-ERR Um, didn't you tell me " + putcrlf(s, "-ERR Um, didn't you tell me " "to delete it already?"); } else { fclose(mfiles[i - 1]); mfiles[i - 1] = NULL; - putpop(s, "+OK Alright man, I got rid of it"); + putcrlf(s, "+OK Alright man, I got rid of it"); } } else if (strcasecmp(linebuf, "QUIT") == 0) { - putpop(s, "+OK See ya, wouldn't want to be ya!"); + putcrlf(s, "+OK See ya, wouldn't want to be ya!"); close(s); break; } else { - putpop(s, "-ERR Um, what?"); + putcrlf(s, "-ERR Um, what?"); } } exit(0); } -/* - * Send one line to the POP client - */ - -static void -putpop(int socket, char *data) -{ - struct iovec iov[2]; - - iov[0].iov_base = data; - iov[0].iov_len = strlen(data); - iov[1].iov_base = "\r\n"; - iov[1].iov_len = 2; - - if (writev(socket, iov, 2) < 0) { - perror ("writev"); - } -} - /* * Put one big buffer to the POP server. Should have already had the line * endings set up and dot-stuffed if necessary. @@ -466,27 +317,3 @@ readmessage(FILE *file) return buffer; } - -/* - * Handle a SIGTERM - */ - -static void -handleterm(int signal) -{ - (void) signal; - - killpidfile(); - fflush(NULL); - _exit(1); -} - -/* - * Get rid of our pid file - */ - -static void -killpidfile(void) -{ - unlink(PIDFILE); -} diff --git a/test/fakesmtp.c b/test/fakesmtp.c index b812f23e..42d4f181 100644 --- a/test/fakesmtp.c +++ b/test/fakesmtp.c @@ -10,35 +10,37 @@ #include #include #include -#include #include #include -#include #include -#include #include -#include -#include #define PIDFILE "/tmp/fakesmtp.pid" #define LINESIZE 1024 -static void killpidfile(void); -static void handleterm(int); -static void putsmtp(int, char *); +enum { + /* Processing top-level SMTP commands (e.g. EHLO, DATA). */ + SMTP_TOP, + + /* Processing payload of a DATA command. */ + SMTP_DATA, + + /* Looking for the blank line required by XOAUTH2 after 334 response. */ + SMTP_XOAUTH_ERR +}; + +void putcrlf(int, char *); +int serve(const char *, const char *); + static int getsmtp(int, char *); int main(int argc, char *argv[]) { - struct addrinfo hints, *res; - int rc, l, conn, on, datamode; - FILE *f, *pid; - pid_t child; - fd_set readfd; - struct stat st; - struct timeval tv; + int rc, conn, smtp_state; + FILE *f; + const char *xoauth = getenv("XOAUTH"); if (argc != 3) { fprintf(stderr, "Usage: %s output-filename port\n", argv[0]); @@ -51,156 +53,14 @@ main(int argc, char *argv[]) exit(1); } - /* - * If there is a pid file already around, kill the previously running - * fakesmtp process. Hopefully this will reduce the race conditions - * that crop up when running the test suite. - */ - - if (stat(PIDFILE, &st) == 0) { - long oldpid; - - if (!(pid = fopen(PIDFILE, "r"))) { - fprintf(stderr, "Cannot open " PIDFILE - " (%s), continuing ...\n", strerror(errno)); - } else { - rc = fscanf(pid, "%ld", &oldpid); - fclose(pid); - - if (rc != 1) { - fprintf(stderr, "Unable to parse pid in " - PIDFILE ", continuing ...\n"); - } else { - kill((pid_t) oldpid, SIGTERM); - } - } - - unlink(PIDFILE); - } - - memset(&hints, 0, sizeof(hints)); - - hints.ai_family = PF_INET; - hints.ai_socktype = SOCK_STREAM; - hints.ai_protocol = IPPROTO_TCP; - hints.ai_flags = AI_PASSIVE; - - rc = getaddrinfo("127.0.0.1", argv[2], &hints, &res); - - if (rc) { - fprintf(stderr, "Unable to resolve localhost/%s: %s\n", - argv[2], gai_strerror(rc)); - exit(1); - } - - l = socket(res->ai_family, res->ai_socktype, res->ai_protocol); - - if (l == -1) { - fprintf(stderr, "Unable to create listening socket: %s\n", - strerror(errno)); - exit(1); - } - - on = 1; - - if (setsockopt(l, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1) { - fprintf(stderr, "Unable to set SO_REUSEADDR: %s\n", - strerror(errno)); - exit(1); - } - - if (bind(l, res->ai_addr, res->ai_addrlen) == -1) { - fprintf(stderr, "Unable to bind socket: %s\n", strerror(errno)); - exit(1); - } - - if (listen(l, 1) == -1) { - fprintf(stderr, "Unable to listen on socket: %s\n", - strerror(errno)); - exit(1); - } - - /* - * Now we fork() and print out the process ID of our child - * for scripts to use. Once we do that, then exit. - */ - - child = fork(); - - switch (child) { - case -1: - fprintf(stderr, "Unable to fork child: %s\n", strerror(errno)); - exit(1); - break; - case 0: - /* - * Close stdin & stdout, otherwise people can - * think we're still doing stuff. For now leave stderr - * open. - */ - fclose(stdin); - fclose(stdout); - break; - default: - printf("%ld\n", (long) child); - exit(0); - } - - /* - * Now that our socket & files are set up, wait 30 seconds for - * a connection. If there isn't one, then exit. - */ - - if (!(pid = fopen(PIDFILE, "w"))) { - fprintf(stderr, "Cannot open " PIDFILE ": %s\n", - strerror(errno)); - exit(1); - } - - fprintf(pid, "%ld\n", (long) getpid()); - fclose(pid); - - signal(SIGTERM, handleterm); - atexit(killpidfile); - - FD_ZERO(&readfd); - FD_SET(l, &readfd); - tv.tv_sec = 30; - tv.tv_usec = 0; - - rc = select(l + 1, &readfd, NULL, NULL, &tv); - - if (rc < 0) { - fprintf(stderr, "select() failed: %s\n", strerror(errno)); - exit(1); - } - - /* - * I think if we get a timeout, we should just exit quietly. - */ - - if (rc == 0) { - exit(1); - } - - /* - * Alright, got a connection! Accept it. - */ - - if ((conn = accept(l, NULL, NULL)) == -1) { - fprintf(stderr, "Unable to accept connection: %s\n", - strerror(errno)); - exit(1); - } + conn = serve(PIDFILE, argv[2]); - close(l); - /* * Pretend to be an SMTP server. */ - putsmtp(conn, "220 Not really an ESMTP server"); - datamode = 0; + putcrlf(conn, "220 Not really an ESMTP server"); + smtp_state = SMTP_TOP; for (;;) { char line[LINESIZE]; @@ -212,17 +72,17 @@ main(int argc, char *argv[]) fprintf(f, "%s\n", line); - /* - * If we're in DATA mode, then check to see if we've got - * a "."; otherwise, continue - */ - - if (datamode) { + switch (smtp_state) { + case SMTP_DATA: if (strcmp(line, ".") == 0) { - datamode = 0; - putsmtp(conn, "250 Thanks for the info!"); + smtp_state = SMTP_TOP; + putcrlf(conn, "250 Thanks for the info!"); } continue; + case SMTP_XOAUTH_ERR: + smtp_state = SMTP_TOP; + putcrlf(conn, "535 Not no way, not no how!"); + continue; } /* @@ -232,15 +92,34 @@ main(int argc, char *argv[]) if (strcmp(line, "QUIT") == 0) { fclose(f); f = NULL; - putsmtp(conn, "221 Later alligator!"); + putcrlf(conn, "221 Later alligator!"); close(conn); break; - } else if (strcmp(line, "DATA") == 0) { - putsmtp(conn, "354 Go ahead"); - datamode = 1; - } else { - putsmtp(conn, "250 I'll buy that for a dollar!"); } + if (strcmp(line, "DATA") == 0) { + putcrlf(conn, "354 Go ahead"); + smtp_state = SMTP_DATA; + continue; + } + if (xoauth != NULL) { + /* XOAUTH2 support enabled; handle EHLO and AUTH. */ + if (strncmp(line, "EHLO", 4) == 0) { + putcrlf(conn, "250-ready"); + putcrlf(conn, "250 AUTH XOAUTH2"); + continue; + } + if (strncmp(line, "AUTH", 4) == 0) { + if (strncmp(line, "AUTH XOAUTH2", 12) == 0 + && strstr(line, xoauth) != NULL) { + putcrlf(conn, "235 OK come in"); + continue; + } + putcrlf(conn, "334 base64-json-err"); + smtp_state = SMTP_XOAUTH_ERR; + continue; + } + } + putcrlf(conn, "250 I'll buy that for a dollar!"); } if (f) @@ -249,25 +128,6 @@ main(int argc, char *argv[]) exit(0); } -/* - * Write a line to the SMTP client on the other end - */ - -static void -putsmtp(int socket, char *data) -{ - struct iovec iov[2]; - - iov[0].iov_base = data; - iov[0].iov_len = strlen(data); - iov[1].iov_base = "\r\n"; - iov[1].iov_len = 2; - - if (writev(socket, iov, 2) < 0) { - perror ("writev"); - } -} - /* * Read a line (up to the \r\n) */ @@ -322,27 +182,3 @@ getsmtp(int socket, char *data) bytesinbuf += cc; } } - -/* - * Handle a SIGTERM - */ - -static void -handleterm(int signal) -{ - (void) signal; - - killpidfile(); - fflush(NULL); - _exit(1); -} - -/* - * Get rid of our pid file - */ - -static void -killpidfile(void) -{ - unlink(PIDFILE); -} diff --git a/test/oauth/common.sh b/test/oauth/common.sh new file mode 100644 index 00000000..5deebfd5 --- /dev/null +++ b/test/oauth/common.sh @@ -0,0 +1,173 @@ +# Common routines for OAuth tests + +. "${MH_OBJ_DIR}/test/common.sh" + +setup_test + +if [ "${OAUTH_SUPPORT}" -eq 0 ]; then + test_skip 'no oauth support' +fi + +testname="${MH_TEST_DIR}/$$" + +arith_eval 64001 + `id -u` % 1000 +http_port=${arith_val} + +arith_eval 64000 + `id -u` % 1000 +pop_port=${arith_val} + +arith_eval 64002 + `id -u` % 1000 +smtp_port=${arith_val} + +cat >> ${MH} < "${pop_message}" < +To: Some Other User +Subject: Hello +Date: Sun, 17 Dec 2006 12:13:14 -0500 + +Hey man +EOM +} + +setup_draft() { + cat > "${MH_TEST_DIR}/Mail/draft" < +To: Somebody Else +Subject: Test +MIME-Version: 1.0 +Content-Type: text/plain; charset="us-ascii" + +This is a test +EOF +} + +start_fakehttp() { + "${MH_OBJ_DIR}/test/fakehttp" "${testname}.http-req" ${http_port} \ + "${testname}.http-res" > /dev/null +} + +start_pop() { + "${MH_OBJ_DIR}/test/fakepop" "${pop_port}" "$1" "$2" "${pop_message}" \ + > /dev/null +} + +start_pop_xoauth() { + start_pop XOAUTH \ + 'dXNlcj1ub2JvZHlAZXhhbXBsZS5jb20BYXV0aD1CZWFyZXIgdGVzdC1hY2Nlc3MBAQ==' +} + +start_fakesmtp() { + "${MH_OBJ_DIR}/test/fakesmtp" "${testname}.smtp-req" ${smtp_port} \ + > /dev/null +} + +fake_creds() { + cat > "${MHTMPDIR}/oauth-test" +} + +fake_http_response() { + echo "HTTP/1.1 $1" > "${testname}.http-res" + cat >> "${testname}.http-res" +} + +fake_json_response() { + (echo 'Content-Type: application/json'; + echo; + cat) | fake_http_response '200 OK' +} + +# The format of the POST request is mostly dependent on curl, and could possibly +# change with newer or older curl versions, or by configuration. curl 7.39.0 +# makes POST requests like this on FreeBSD 10 and Ubuntu 14.04. If you find +# this failing, you'll need to make this a smarter comparison. +expect_http_post() { + cat > "${testname}.expected-http-req" < "${testname}.expected-creds" +} + +test_inc() { + run_test "inc -host 127.0.0.1 -port ${pop_port} -oauth test -user nobody@example.com -width 80" "$@" +} + +test_inc_success() { + test_inc 'Incorporating new mail into inbox... + + 11+ 12/17 No Such User Hello<>' + check "${pop_message}" "`mhpath +inbox 11`" 'keep first' +} + +test_send_no_servers() { + run_test "send -draft -server 127.0.0.1 -port ${smtp_port} -oauth test -user nobody@example.com" "$@" +} + +test_send_only_fakesmtp() { + start_fakesmtp + test_send_no_servers "$@" +} + +test_send() { + start_fakehttp + test_send_only_fakesmtp "$@" + check "${testname}.http-req" "${testname}.expected-http-req" +} + +check_http_req() { + check "${testname}.http-req" "${testname}.expected-http-req" +} + +check_creds_private() { + f="${MHTMPDIR}/oauth-test" + if ls -dl "$f" | grep '^-rw-------' > /dev/null 2>&1; then + : + else + echo "$f permissions not private" + failed=`expr ${failed:-0} + 1` + fi +} + +check_creds() { + # It's hard to calculate the exact expiration time mhlogin is going to use, + # so we'll just use sed to remove the actual time so we can easily compare + # it against our "correct" output. + f="${MHTMPDIR}/oauth-test" + + sed 's/^expire:.*/expire:/' "$f" > "$f".notime + check "$f".notime "${testname}.expected-creds" + rm "$f" +} diff --git a/test/oauth/test-inc b/test/oauth/test-inc new file mode 100755 index 00000000..8c71e624 --- /dev/null +++ b/test/oauth/test-inc @@ -0,0 +1,116 @@ +#!/bin/sh +# +# Test the XOAUTH2 support in inc +# + +if test -z "${MH_OBJ_DIR}"; then + srcdir=`dirname "$0"`/../.. + MH_OBJ_DIR=`cd "${srcdir}" && pwd`; export MH_OBJ_DIR +fi + +. "${srcdir}/test/oauth/common.sh" + +setup_pop + +# +# success cases +# + +# TEST +echo 'access token ready, pop server accepts message' + +fake_creds < "${testname}.expected-creds" + cat /dev/null > "${MHTMPDIR}/oauth-test" + chmod 600 "${MHTMPDIR}/oauth-test" +} + +test_mhlogin() { + start_fakehttp + run_test 'eval echo code | mhlogin -oauth test' \ +"Load the following URL in your browser and authorize nmh to access test: + +http://127.0.0.1:${http_port}/oauth/auth?response_type=code&client_id=test-id&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&scope=test-scope + +Enter the authorization code: $1" + check_http_req + check_creds_private + check_creds +} + +test_mhlogin_invalid_response() { + test_mhlogin 'mhlogin: error exchanging code for OAuth2 token +mhlogin: invalid response' +} + +# +# success cases +# + +# TEST +echo 'mhlogin receives access and expiration' + +expect_http_post_code + +fake_json_response < "${testname}.expected-send-output" < "${testname}.send-output" 2>&1 || true +# Clear out an error message we get from libcurl on some systems (seen at least +# 3 different versions of this error message, on FreeBSD 10.1, Ubuntu 12.04, and +# Ubuntu 14.04). +f="${testname}.send-output" +sed 's/\(send: error making HTTP request to OAuth2 authorization endpoint:\).*/\1 [details]/' "$f" > "$f".clean +check "$f".clean "${testname}.expected-send-output" +rm "$f" + +# TEST +echo 'refresh gets bogus 200 response from http server' + +expect_http_post_refresh + +fake_http_response '200 OK' <doh! +EOF + +test_send_only_fakehttp 'send: error refreshing OAuth2 token +send: invalid response' + +# TEST +echo 'refresh gets 500 response from http server' + +expect_http_post_refresh + +fake_http_response '500 Server Error' <doh! +EOF + +test_send_only_fakehttp 'send: error refreshing OAuth2 token +send: invalid response' + +# TEST +echo 'refresh gets proper error from http' + +expect_http_post_refresh + +fake_http_response '400 Bad Request' <> "${testname}.http-res" + +test_send_only_fakehttp 'send: error refreshing OAuth2 token +send: refusing to process response body larger than 8192 bytes' + +# TEST +echo 'smtp server rejects token' + +XOAUTH='not-that-one' + +fake_creds < +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static const char *PIDFN = NULL; + +static void killpidfile(void); +static void handleterm(int); + +static int +try_bind(int socket, const struct sockaddr *address, socklen_t len) +{ + int i, status; + for (i = 0; i < 5; i++) { + if ((status = bind(socket, address, len)) == 0) { + return 0; + } + sleep(1); + } + + return status; +} + +int +serve(const char *pidfn, const char *port) +{ + struct addrinfo hints, *res; + int rc, l, conn, on; + FILE *pid; + pid_t child; + fd_set readfd; + struct stat st; + struct timeval tv; + + PIDFN = pidfn; + + /* + * If there is a pid file already around, kill the previously running + * fakesmtp process. Hopefully this will reduce the race conditions + * that crop up when running the test suite. + */ + + if (stat(pidfn, &st) == 0) { + long oldpid; + + if (!(pid = fopen(pidfn, "r"))) { + fprintf(stderr, "Cannot open %s (%s), continuing ...\n", + pidfn, strerror(errno)); + } else { + rc = fscanf(pid, "%ld", &oldpid); + fclose(pid); + + if (rc != 1) { + fprintf(stderr, "Unable to parse pid in %s," + " continuing ...\n", + pidfn); + } else { + kill((pid_t) oldpid, SIGTERM); + } + } + + unlink(pidfn); + } + + memset(&hints, 0, sizeof(hints)); + + hints.ai_family = PF_INET; + hints.ai_socktype = SOCK_STREAM; + hints.ai_protocol = IPPROTO_TCP; + hints.ai_flags = AI_PASSIVE; + + rc = getaddrinfo("127.0.0.1", port, &hints, &res); + + if (rc) { + fprintf(stderr, "Unable to resolve localhost/%s: %s\n", + port, gai_strerror(rc)); + exit(1); + } + + l = socket(res->ai_family, res->ai_socktype, res->ai_protocol); + + if (l == -1) { + fprintf(stderr, "Unable to create listening socket: %s\n", + strerror(errno)); + exit(1); + } + + on = 1; + + if (setsockopt(l, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1) { + fprintf(stderr, "Unable to set SO_REUSEADDR: %s\n", + strerror(errno)); + exit(1); + } + + if (try_bind(l, res->ai_addr, res->ai_addrlen) == -1) { + fprintf(stderr, "Unable to bind socket: %s\n", strerror(errno)); + exit(1); + } + + if (listen(l, 1) == -1) { + fprintf(stderr, "Unable to listen on socket: %s\n", + strerror(errno)); + exit(1); + } + + /* + * Now we fork() and print out the process ID of our child + * for scripts to use. Once we do that, then exit. + */ + + child = fork(); + + switch (child) { + case -1: + fprintf(stderr, "Unable to fork child: %s\n", strerror(errno)); + exit(1); + break; + case 0: + /* + * Close stdin & stdout, otherwise people can + * think we're still doing stuff. For now leave stderr + * open. + */ + fclose(stdin); + fclose(stdout); + break; + default: + /* XXX why? it's never used... */ + printf("%ld\n", (long) child); + exit(0); + } + + /* + * Now that our socket & files are set up, wait 30 seconds for + * a connection. If there isn't one, then exit. + */ + + if (!(pid = fopen(pidfn, "w"))) { + fprintf(stderr, "Cannot open %s: %s\n", + pidfn, strerror(errno)); + exit(1); + } + + fprintf(pid, "%ld\n", (long) getpid()); + fclose(pid); + + signal(SIGTERM, handleterm); + atexit(killpidfile); + + FD_ZERO(&readfd); + FD_SET(l, &readfd); + tv.tv_sec = 30; + tv.tv_usec = 0; + + rc = select(l + 1, &readfd, NULL, NULL, &tv); + + if (rc < 0) { + fprintf(stderr, "select() failed: %s\n", strerror(errno)); + exit(1); + } + + /* + * I think if we get a timeout, we should just exit quietly. + */ + + if (rc == 0) { + exit(1); + } + + /* + * Alright, got a connection! Accept it. + */ + + if ((conn = accept(l, NULL, NULL)) == -1) { + fprintf(stderr, "Unable to accept connection: %s\n", + strerror(errno)); + exit(1); + } + + close(l); + + return conn; +} + +/* + * Write a line (adding \r\n) to the client on the other end + */ +void +putcrlf(int socket, char *data) +{ + struct iovec iov[2]; + + iov[0].iov_base = data; + iov[0].iov_len = strlen(data); + iov[1].iov_base = "\r\n"; + iov[1].iov_len = 2; + + /* ECONNRESET just means the client already closed its end */ + /* XXX is it useful to log errors here at all? */ + if (writev(socket, iov, 2) < 0 && errno != ECONNRESET) { + perror ("writev"); + } +} + +/* + * Handle a SIGTERM + */ + +static void +handleterm(int signal) +{ + (void) signal; + + killpidfile(); + fflush(NULL); + _exit(1); +} + +/* + * Get rid of our pid file + */ + +static void +killpidfile(void) +{ + if (PIDFN != NULL) { + unlink(PIDFN); + } +} diff --git a/uip/inc.c b/uip/inc.c index 8b9fe7ae..a4aea3e7 100644 --- a/uip/inc.c +++ b/uip/inc.c @@ -52,6 +52,7 @@ X("form formatfile", 0, FORMSW) \ X("format string", 5, FMTSW) \ X("host hostname", 0, HOSTSW) \ + X("oauth service", 0, OAUTHSW) \ X("user username", 0, USERSW) \ X("pack file", 0, PACKSW) \ X("nopack", 0, NPACKSW) \ @@ -185,10 +186,10 @@ main (int argc, char **argv) FILE *aud = NULL; char b[PATH_MAX + 1]; char *maildir_copy = NULL; /* copy of mail directory because the static gets overwritten */ + const char *oauth_svc = NULL; int nmsgs, nbytes; char *MAILHOST_env_variable; - done=inc_done; /* absolutely the first thing we do is save our privileges, @@ -313,6 +314,16 @@ main (int argc, char **argv) adios (NULL, "missing argument to %s", argp[-2]); continue; + case OAUTHSW: +#ifdef OAUTH_SUPPORT + if (!(cp = *argp++) || *cp == '-') + adios (NULL, "missing argument to %s", argp[-2]); + oauth_svc = cp; +#else + adios (NULL, "not built with OAuth support"); +#endif + continue; + case USERSW: if (!(user = *argp++) || *user == '-') adios (NULL, "missing argument to %s", argp[-2]); @@ -383,12 +394,20 @@ main (int argc, char **argv) if (inc_type == INC_POP) { struct nmh_creds creds = { 0, 0, 0 }; + if (oauth_svc == NULL) { + nmh_get_credentials (host, user, sasl, &creds); + } else { + if (user == NULL) { + adios (NULL, "must specify -user with -oauth"); + } + creds.user = user; + } + /* * initialize POP connection */ - nmh_get_credentials (host, user, sasl, &creds); if (pop_init (host, port, creds.user, creds.password, proxy, snoop, - sasl, saslmech) == NOTOK) + sasl, saslmech, oauth_svc) == NOTOK) adios (NULL, "%s", response); /* Check if there are any messages */ diff --git a/uip/mhlogin.c b/uip/mhlogin.c new file mode 100644 index 00000000..4fa10e1e --- /dev/null +++ b/uip/mhlogin.c @@ -0,0 +1,162 @@ +/* + * mhlogin.c -- login to external (OAuth) services + * + * This code is Copyright (c) 2014, by the authors of nmh. See the + * COPYRIGHT file in the root directory of the nmh distribution for + * complete copyright information. + */ + +#include +#include + +#include +#include + +#define MHLOGIN_SWITCHES \ + X("oauth", 1, OAUTHSW) \ + X("snoop", 1, SNOOPSW) \ + X("help", 1, HELPSW) \ + X("version", 1, VERSIONSW) \ + +#define X(sw, minchars, id) id, +DEFINE_SWITCH_ENUM(MHLOGIN); +#undef X + +#define X(sw, minchars, id) { sw, minchars, id }, +DEFINE_SWITCH_ARRAY(MHLOGIN, switches); +#undef X + +#ifdef OAUTH_SUPPORT +/* XXX copied from install-mh.c */ +static char * +geta (void) +{ + char *cp; + static char line[BUFSIZ]; + + if (fgets(line, sizeof(line), stdin) == NULL) + done (1); + if ((cp = strchr(line, '\n'))) + *cp = 0; + return line; +} + +static int +do_login(const char *svc, int snoop) +{ + char *fn, *code; + mh_oauth_ctx *ctx; + mh_oauth_cred *cred; + FILE *cred_file; + int failed_to_lock = 0; + const char *url; + + if (svc == NULL) { + adios(NULL, "only support -oauth gmail"); + } + + if (!mh_oauth_new(&ctx, svc)) { + adios(NULL, mh_oauth_get_err_string(ctx)); + } + + if (snoop) { + mh_oauth_log_to(stderr, ctx); + } + + fn = getcpy(mh_oauth_cred_fn(ctx)); + + if ((url = mh_oauth_get_authorize_url(ctx)) == NULL) { + adios(NULL, mh_oauth_get_err_string(ctx)); + } + + printf("Load the following URL in your browser and authorize nmh" + " to access %s:\n" + "\n%s\n\n" + "Enter the authorization code: ", + mh_oauth_svc_display_name(ctx), url); + fflush(stdout); + code = geta(); + + while ((cred = mh_oauth_authorize(code, ctx)) == NULL + && mh_oauth_get_err_code(ctx) == MH_OAUTH_BAD_GRANT) { + printf("Code rejected; try again? "); + fflush(stdout); + code = geta(); + } + if (cred == NULL) { + advise(NULL, "error exchanging code for OAuth2 token"); + adios(NULL, mh_oauth_get_err_string(ctx)); + } + + cred_file = lkfopendata(fn, "w", &failed_to_lock); + if (cred_file == NULL || failed_to_lock) { + adios(fn, "oops"); + } + if (!mh_oauth_cred_save(cred_file, cred)) { + adios(NULL, mh_oauth_get_err_string(ctx)); + } + if (lkfclosedata(cred_file, fn) != 0) { + adios (fn, "oops"); + } + + mh_oauth_cred_free(cred); + mh_oauth_free(ctx); + + return 0; +} +#endif + +int +main(int argc, char **argv) +{ + char *cp, **argp, **arguments; + char *svc = NULL; + int snoop = 0; + + if (nmh_init(argv[0], 1)) { return 1; } + + arguments = getarguments (invo_name, argc, argv, 1); + argp = arguments; + + while ((cp = *argp++)) { + if (*cp == '-') { + char help[BUFSIZ]; + switch (smatch (++cp, switches)) { + case AMBIGSW: + ambigsw (cp, switches); + done (1); + case UNKWNSW: + adios (NULL, "-%s unknown", cp); + + case HELPSW: + snprintf(help, sizeof(help), "%s -oauth gmail [switches]", + invo_name); + print_help (help, switches, 1); + done (0); + case VERSIONSW: + print_version(invo_name); + done (0); + + case OAUTHSW: + if (!(cp = *argp++) || *cp == '-') + adios (NULL, "missing argument to %s", argp[-2]); + svc = cp; + continue; + + case SNOOPSW: + snoop++; + continue; + } + } + adios(NULL, "extraneous arguments"); + } + +#ifdef OAUTH_SUPPORT + return do_login(svc, snoop); +#else + NMH_UNUSED(svc); + NMH_UNUSED(snoop); + adios(NULL, "not built with OAuth support"); + return 1; +#endif +} diff --git a/uip/msgchk.c b/uip/msgchk.c index 49e758f0..ef9d32b6 100644 --- a/uip/msgchk.c +++ b/uip/msgchk.c @@ -33,6 +33,7 @@ X("snoop", -5, SNOOPSW) \ X("sasl", SASLminc(-4), SASLSW) \ X("saslmech", SASLminc(-5), SASLMECHSW) \ + X("oauth service", 0, OAUTHSW) \ X("proxy command", 0, PROXYSW) \ #define X(sw, minchars, id) id, @@ -71,7 +72,7 @@ DEFINE_SWITCH_ARRAY(MSGCHK, switches); static int donote (char *, int); static int checkmail (char *, char *, int, int, int); static int remotemail (char *, char *, char *, char *, int, int, int, int, - char *); + char *, const char *); int @@ -84,6 +85,7 @@ main (int argc, char **argv) char buf[BUFSIZ], *saslmech = NULL; char **argp, **arguments, *vec[MAXVEC]; struct passwd *pw; + const char *oauth_svc = NULL; if (nmh_init(argv[0], 1)) { return 1; } @@ -138,6 +140,16 @@ main (int argc, char **argv) adios (NULL, "missing argument to %s", argp[-2]); continue; + case OAUTHSW: +#ifdef OAUTH_SUPPORT + if (!(cp = *argp++) || *cp == '-') + adios (NULL, "missing argument to %s", argp[-2]); + oauth_svc = cp; +#else + adios (NULL, "not built with OAuth support"); +#endif + continue; + case USERSW: if (!(cp = *argp++) || *cp == '-') adios (NULL, "missing argument to %s", argp[-2]); @@ -192,11 +204,11 @@ main (int argc, char **argv) if (host) { if (vecp == 0) { status = remotemail (host, port, user, proxy, notifysw, 1, - snoop, sasl, saslmech); + snoop, sasl, saslmech, oauth_svc); } else { for (vecp = 0; vec[vecp]; vecp++) status += remotemail (host, port, vec[vecp], proxy, notifysw, 0, - snoop, sasl, saslmech); + snoop, sasl, saslmech, oauth_svc); } } else { if (user == NULL) user = getusername (); @@ -320,15 +332,24 @@ extern char response[]; static int remotemail (char *host, char *port, char *user, char *proxy, int notifysw, - int personal, int snoop, int sasl, char *saslmech) + int personal, int snoop, int sasl, char *saslmech, + const char *oauth_svc) { int nmsgs, nbytes, status; struct nmh_creds creds = { 0, 0, 0 }; + if (oauth_svc == NULL) { + nmh_get_credentials (host, user, sasl, &creds); + } else { + if (user == NULL) { + adios (NULL, "must specify -user with -oauth"); + } + creds.user = user; + } + /* open the POP connection */ - nmh_get_credentials (host, user, sasl, &creds); if (pop_init (host, port, creds.user, creds.password, proxy, snoop, sasl, - saslmech) == NOTOK + saslmech, oauth_svc) == NOTOK || pop_stat (&nmsgs, &nbytes) == NOTOK /* check for messages */ || pop_quit () == NOTOK) { /* quit POP connection */ advise (NULL, "%s", response); diff --git a/uip/popsbr.c b/uip/popsbr.c index ccc76c7c..5b99c8cf 100644 --- a/uip/popsbr.c +++ b/uip/popsbr.c @@ -8,6 +8,7 @@ #include #include +#include #ifdef CYRUS_SASL # include @@ -80,34 +81,10 @@ static int sasl_getline (char *, int, FILE *); static int putline (char *, FILE *); -#ifdef CYRUS_SASL -/* - * This function implements the AUTH command for various SASL mechanisms - * - * We do the whole SASL dialog here. If this completes, then we've - * authenticated successfully and have (possibly) negotiated a security - * layer. - */ - -#define CHECKB64SIZE(insize, outbuf, outsize) \ - { size_t wantout = (((insize + 2) / 3) * 4) + 32; \ - if (wantout > outsize) { \ - outbuf = mh_xrealloc(outbuf, outsize = wantout); \ - } \ - } - int -pop_auth_sasl(char *user, char *host, char *mech) +check_mech(char *server_mechs, size_t server_mechs_size, char *mech) { - int result, status, sasl_capability = 0; - unsigned int buflen, outlen; - char server_mechs[256], *buf, *outbuf = NULL; - size_t outbufsize = 0; - const char *chosen_mech; - sasl_security_properties_t secprops; - struct pass_context p_context; - sasl_ssf_t *ssf; - int *moutbuf; + int status, sasl_capability = 0; /* * First off, we're going to send the CAPA command to see if we can @@ -137,7 +114,7 @@ pop_auth_sasl(char *user, char *host, char *mech) * We've seen the SASL capability. Grab the mech list */ sasl_capability++; - strncpy(server_mechs, response + 5, sizeof(server_mechs)); + strncpy(server_mechs, response + 5, server_mechs_size); } break; } @@ -159,6 +136,42 @@ pop_auth_sasl(char *user, char *host, char *mech) return NOTOK; } + return OK; +} + +#ifdef CYRUS_SASL +/* + * This function implements the AUTH command for various SASL mechanisms + * + * We do the whole SASL dialog here. If this completes, then we've + * authenticated successfully and have (possibly) negotiated a security + * layer. + */ + +#define CHECKB64SIZE(insize, outbuf, outsize) \ + { size_t wantout = (((insize + 2) / 3) * 4) + 32; \ + if (wantout > outsize) { \ + outbuf = mh_xrealloc(outbuf, outsize = wantout); \ + } \ + } + +int +pop_auth_sasl(char *user, char *host, char *mech) +{ + int result, status; + unsigned int buflen, outlen; + char server_mechs[256], *buf, *outbuf = NULL; + size_t outbufsize = 0; + const char *chosen_mech; + sasl_security_properties_t secprops; + struct pass_context p_context; + sasl_ssf_t *ssf; + int *moutbuf; + + if ((status = check_mech(server_mechs, sizeof(server_mechs), mech)) != OK) { + return status; + } + /* * Start the SASL process. First off, initialize the SASL library. */ @@ -422,6 +435,26 @@ sasl_get_pass(sasl_conn_t *conn, void *context, int id, sasl_secret_t **psecret) } #endif /* CYRUS_SASL */ +int +pop_auth_xoauth(const char *client_res) +{ + char server_mechs[256]; + int status = check_mech(server_mechs, sizeof(server_mechs), "XOAUTH"); + + if (status != OK) return status; + + if ((status = command("AUTH XOAUTH2 %s", client_res)) != OK) { + return status; + } + if (strncmp(response, "+OK", 3) == 0) { + return OK; + } + + /* response contains base64-encoded JSON, which is always the same. + * See mts/smtp/smtp.c for more notes on that. */ + /* Then we're supposed to send an empty response ("\r\n"). */ + return command(""); +} /* * Split string containing proxy command into an array of arguments @@ -474,15 +507,26 @@ parse_proxy(char *proxy, char *host) int pop_init (char *host, char *port, char *user, char *pass, char *proxy, - int snoop, int sasl, char *mech) + int snoop, int sasl, char *mech, const char *oauth_svc) { int fd1, fd2; char buffer[BUFSIZ]; + const char *xoauth_client_res = NULL; #ifndef CYRUS_SASL NMH_UNUSED (sasl); NMH_UNUSED (mech); #endif /* ! CYRUS_SASL */ +#ifdef OAUTH_SUPPORT + if (oauth_svc != NULL) { + xoauth_client_res = mh_oauth_do_xoauth(user, oauth_svc, + snoop ? stderr : NULL); + } +#else + NMH_UNUSED (oauth_svc); + NMH_UNUSED (xoauth_client_res); +#endif /* OAUTH_SUPPORT */ + if (proxy && *proxy) { int pid; int inpipe[2]; /* for reading from the server */ @@ -566,6 +610,12 @@ pop_init (char *host, char *port, char *user, char *pass, char *proxy, return OK; } else # endif /* CYRUS_SASL */ +# if OAUTH_SUPPORT + if (xoauth_client_res != NULL) { + if (pop_auth_xoauth(xoauth_client_res) != NOTOK) + return OK; + } else +# endif /* OAUTH_SUPPORT */ if (command ("USER %s", user) != NOTOK && command ("%s %s", (pophack++, "PASS"), pass) != NOTOK) diff --git a/uip/post.c b/uip/post.c index 685129b9..0beab55b 100644 --- a/uip/post.c +++ b/uip/post.c @@ -80,6 +80,7 @@ X("nosasl", SASLminc(-6), NOSASLSW) \ X("saslmaxssf", SASLminc(-10), SASLMXSSFSW) \ X("saslmech", SASLminc(-5), SASLMECHSW) \ + X("oauth", -5, OAUTHSW) \ X("user", SASLminc(-4), USERSW) \ X("port server submission port name/number", 4, PORTSW) \ X("tls", TLSminc(-3), TLSSW) \ @@ -256,14 +257,14 @@ static void anno (void); static int annoaux (struct mailname *); static void insert_fcc (struct headers *, char *); static void make_bcc_file (int); -static void verify_all_addresses (int, char *); +static void verify_all_addresses (int, char *, const char *); static void chkadr (void); static void sigon (void); static void sigoff (void); static void p_refile (char *); static void fcc (char *, char *); static void die (char *, char *, ...); -static void post (char *, int, int, char *); +static void post (char *, int, int, char *, const char *); static void do_text (char *file, int fd); static void do_an_address (struct mailname *, int); static void do_addresses (int, int); @@ -278,6 +279,7 @@ main (int argc, char **argv) char buf[BUFSIZ], name[NAMESZ]; FILE *in, *out; m_getfld_state_t gstate = 0; + char *xoauth_client_res = NULL; if (nmh_init(argv[0], 0 /* use context_foil() */)) { return 1; } @@ -438,7 +440,13 @@ main (int argc, char **argv) if (!(saslmech = *argp++) || *saslmech == '-') adios (NULL, "missing argument to %s", argp[-2]); continue; - + + case OAUTHSW: + if (!(cp = *argp++) || *cp == '-') + adios (NULL, "missing argument to %s", argp[-2]); + xoauth_client_res = cp; + continue; + case USERSW: if (!(user = *argp++) || *user == '-') adios (NULL, "missing argument to %s", argp[-2]); @@ -620,7 +628,7 @@ main (int argc, char **argv) /* If we are doing a "whom" check */ if (whomsw) { /* This won't work with MTS_SENDMAIL_PIPE. */ - verify_all_addresses (1, envelope); + verify_all_addresses (1, envelope, xoauth_client_res); done (0); } @@ -632,14 +640,14 @@ main (int argc, char **argv) verify_all_addresses with MTS_SENDMAIL_PIPE, but that might require running sendmail as root. Note that spost didn't verify addresses. */ - verify_all_addresses (verbose, envelope); + verify_all_addresses (verbose, envelope, xoauth_client_res); } - post (tmpfil, 0, verbose, envelope); + post (tmpfil, 0, verbose, envelope, xoauth_client_res); } - post (bccfil, 1, verbose, envelope); + post (bccfil, 1, verbose, envelope, xoauth_client_res); (void) m_unlink (bccfil); } else { - post (tmpfil, 0, isatty (1), envelope); + post (tmpfil, 0, isatty (1), envelope, xoauth_client_res); } p_refile (tmpfil); @@ -1486,7 +1494,8 @@ do_addresses (int bccque, int talk) */ static void -post (char *file, int bccque, int talk, char *envelope) +post (char *file, int bccque, int talk, char *envelope, + const char *xoauth_client_res) { int fd; int retval, i; @@ -1536,8 +1545,8 @@ post (char *file, int bccque, int talk, char *envelope) } else { if (rp_isbad (retval = sm_init (clientsw, serversw, port, watch, verbose, snoop, sasl, saslssf, - saslmech, user, tls)) || - rp_isbad (retval = sm_winit (envelope))) + saslmech, user, xoauth_client_res, tls)) + || rp_isbad (retval = sm_winit (envelope))) die (NULL, "problem initializing server; %s", rp_string (retval)); do_addresses (bccque, talk && verbose); @@ -1566,7 +1575,7 @@ post (char *file, int bccque, int talk, char *envelope) /* Address Verification */ static void -verify_all_addresses (int talk, char *envelope) +verify_all_addresses (int talk, char *envelope, const char *xoauth_client_res) { int retval; struct mailname *lp; @@ -1576,7 +1585,7 @@ verify_all_addresses (int talk, char *envelope) if (!whomsw || checksw) if (rp_isbad (retval = sm_init (clientsw, serversw, port, watch, verbose, snoop, sasl, saslssf, - saslmech, user, tls)) + saslmech, user, xoauth_client_res, tls)) || rp_isbad (retval = sm_winit (envelope))) die (NULL, "problem initializing server; %s", rp_string (retval)); diff --git a/uip/send.c b/uip/send.c index 5885c98e..4c022b4c 100644 --- a/uip/send.c +++ b/uip/send.c @@ -10,6 +10,8 @@ #include #include +#include +#include #ifndef CYRUS_SASL # define SASLminc(a) (a) @@ -61,6 +63,7 @@ X("nosasl", SASLminc(-6), NOSASLSW) \ X("saslmaxssf", SASLminc(-10), SASLMXSSFSW) \ X("saslmech mechanism", SASLminc(-5), SASLMECHSW) \ + X("oauth service", 0, OAUTHSW) \ X("user username", SASLminc(-4), USERSW) \ X("attach", -6, ATTACHSW) \ X("noattach", -8, NOATTACHSW) \ @@ -115,8 +118,10 @@ main (int argc, char **argv) char *cp, *dfolder = NULL, *maildir = NULL; char buf[BUFSIZ], **ap, **argp, **arguments, *program; char *msgs[MAXARGS], **vec; + const char *user = NULL, *oauth_svc = NULL; struct msgs *mp; struct stat st; + int snoop = 0; if (nmh_init(argv[0], 1)) { return 1; } @@ -227,6 +232,11 @@ main (int argc, char **argv) vec[vecp++] = --cp; continue; + case SNOOPSW: + snoop++; + vec[vecp++] = --cp; + continue; + case DEBUGSW: debugsw++; /* fall */ case NFILTSW: @@ -238,7 +248,6 @@ main (int argc, char **argv) case NMSGDSW: case WATCSW: case NWATCSW: - case SNOOPSW: case SASLSW: case NOSASLSW: case TLSSW: @@ -247,6 +256,25 @@ main (int argc, char **argv) vec[vecp++] = --cp; continue; + case OAUTHSW: +#ifdef OAUTH_SUPPORT + if (!(cp = *argp++) || *cp == '-') + adios (NULL, "missing argument to %s", argp[-2]); + oauth_svc = cp; +#else + NMH_UNUSED (oauth_svc); + adios (NULL, "not built with OAuth support"); +#endif + continue; + + case USERSW: + vec[vecp++] = --cp; + if (!(cp = *argp++) || *cp == '-') + adios (NULL, "missing argument to %s", argp[-2]); + vec[vecp++] = cp; + user = cp; + continue; + case ALIASW: case FILTSW: case WIDTHSW: @@ -254,7 +282,6 @@ main (int argc, char **argv) case SERVSW: case SASLMECHSW: case SASLMXSSFSW: - case USERSW: case PORTSW: case MTSSW: case MESSAGEIDSW: @@ -416,6 +443,18 @@ go_to_it: distfile = NULL; } +#ifdef OAUTH_SUPPORT + if (oauth_svc != NULL) { + if (user == NULL) { + adios (NULL, "must specify -user with -oauth"); + } + + vec[vecp++] = "-oauth"; + vec[vecp++] = mh_oauth_do_xoauth (user, oauth_svc, + snoop ? stderr : NULL); + } +#endif /* OAUTH_SUPPORT */ + if (altmsg == NULL || stat (altmsg, &st) == NOTOK) { st.st_mtime = 0; st.st_dev = 0;