]> diplodocus.org Git - nmh/commitdiff
Implement OAuth 2.0 [1] for XOAUTH2 in SMTP [2] and POP3 [3].
authorEric Gillespie <epg@google.com>
Tue, 9 Dec 2014 07:20:01 +0000 (23:20 -0800)
committerEric Gillespie <epg@google.com>
Tue, 9 Dec 2014 07:20:01 +0000 (23:20 -0800)
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.

32 files changed:
.gitignore
Makefile.am
config/version.sh
configure.ac
h/mh.h
h/oauth.h [new file with mode: 0644]
h/popsbr.h
h/prototypes.h
man/inc.man
man/mhlogin.man [new file with mode: 0644]
man/msgchk.man
man/send.man
mts/smtp/smtp.c
mts/smtp/smtp.h
sbr/error.c
sbr/oauth.c [new file with mode: 0644]
sbr/readconfig.c
test/fakehttp.c [new file with mode: 0644]
test/fakepop.c
test/fakesmtp.c
test/oauth/common.sh [new file with mode: 0644]
test/oauth/test-inc [new file with mode: 0755]
test/oauth/test-mhlogin [new file with mode: 0755]
test/oauth/test-send [new file with mode: 0755]
test/oauth/test-share [new file with mode: 0755]
test/server.c [new file with mode: 0644]
uip/inc.c
uip/mhlogin.c [new file with mode: 0644]
uip/msgchk.c
uip/popsbr.c
uip/post.c
uip/send.c

index 4e9297dbb07e042bd42b014e8398f593b36a1436..27038108db16ed1bb4b43583fa82b1531f0acda6 100644 (file)
@@ -80,6 +80,7 @@ a.out.dSYM/
 /uip/mhfixmsg
 /uip/mhl
 /uip/mhlist
 /uip/mhfixmsg
 /uip/mhl
 /uip/mhlist
+/uip/mhlogin
 /uip/mhn
 /uip/mhparam
 /uip/mhpath
 /uip/mhn
 /uip/mhparam
 /uip/mhpath
@@ -110,6 +111,7 @@ a.out.dSYM/
 /uip/whatnow
 /uip/whom
 /uip/*.exe
 /uip/whatnow
 /uip/whom
 /uip/*.exe
+/test/fakehttp
 /test/fakepop
 /test/fakesmtp
 /test/getcanon
 /test/fakepop
 /test/fakesmtp
 /test/getcanon
index 1cc087532aa8bf09d1d2fefb778a291542e7d37a..59269d5106a1487a43922844dcc2fe11154b86a3 100644 (file)
@@ -34,6 +34,9 @@ nmhlibexecdir = @libexecdir@/nmh
 ## nmh _does_ have a test suite!
 ##
 TESTS_ENVIRONMENT = MH_OBJ_DIR="@abs_builddir@" \
 ## 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)" \
                    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/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 \
        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 \
 
 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
 
 ##
 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/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
 
 
 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 \
                 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
 
 ##
 ## 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/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
 
 ##
 ## 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/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
 
 ##
 ## 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/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)
 
 ##
             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_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)
 
 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_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)
 
 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_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)
 
 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_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)
 
 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_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_fakepop_LDADD = $(POSTLINK)
 
-test_fakesmtp_SOURCES = test/fakesmtp.c
+test_fakesmtp_SOURCES = test/fakesmtp.c test/server.c
 test_fakesmtp_LDADD = $(POSTLINK)
 
 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)
 
 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 \
                      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,
 
 ##
 ## Because these files use the definitions in the libmh rule below,
index bf95447d6971eb771057bc7ba2f940b936c00fc7..a0105e3626b2b04423eb3615ad39c866432ce277 100755 (executable)
@@ -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 *version_str = \"nmh-$VERSION [compiled on $HOSTNAME at `date`]\";"
 fi
 echo "char *version_num = \"nmh-$VERSION\";"
+echo "char *user_agent = \"nmh/$VERSION\";"
index e0a96f5db13f2187ebe0c1adf2cd4ce4d716ac04..507e4eae92802cc7403b849876ae5b12f4087b95 100644 (file)
@@ -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])
 
            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"],[
 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])
 
   [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 ----------------
 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}
 default smtp servers       : ${smtpservers}
 SASL support               : ${sasl_support}
 TLS support                : ${tls_support}
+OAuth support              : ${oauth_support}
 ])])dnl
 
 dnl ---------------
 ])])dnl
 
 dnl ---------------
diff --git a/h/mh.h b/h/mh.h
index b8c60ca80c86d067eeca787201fface5a1c505ad..71315b6ead493140dde04c36ff3bd11b0f24f3df 100644 (file)
--- 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 *showmimeproc;
 extern char *showproc;
 extern char *usequence;
+extern char *user_agent;
 extern char *version_num;
 extern char *version_str;
 extern char *whatnowproc;
 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 (file)
index 0000000..a49cb4a
--- /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);
index fc06f0b987067ae0469edb5fb3ca2b9a8bcb23d3..3fb4179dba5560228f01be561550451bc6d85d35 100644 (file)
@@ -3,7 +3,8 @@
  * popsbr.h -- header for POP client subroutines
  */
 
  * 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 *));
 int pop_fd (char *, int, char *, int);
 int pop_stat (int *, int *);
 int pop_retr (int, int (*)(char *));
index a338fd52765b6ec5277f33509ecbbdf4adfef603..5c104a7e585c00ab9d4de0ef9f6360b81855f6fa 100644 (file)
@@ -18,9 +18,9 @@ char *etcpath(char *);
 struct msgs_array;
 
 void add_profile_entry (const char *, const 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 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 **);
 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 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 *);
 int refile (char **, char *);
 void ruserpass (char *, char **, char **);
 int remdir (char *);
index 61f6f8442ae4baa1d075c992f68975bf5e8079e1..f72b1f21a999d06586966924332f360bb9c4c418 100644 (file)
@@ -1,4 +1,4 @@
-.TH INC %manext1% "April 18, 2014" "%nmhversion%"
+.TH INC %manext1% "November 25, 2014" "%nmhversion%"
 .\"
 .\" %nmhwarning%
 .\"
 .\"
 .\" %nmhwarning%
 .\"
@@ -37,6 +37,8 @@ inc \- incorporate new mail
 .RB [ \-sasl " | " \-nosasl ]
 .RB [ \-saslmech
 .IR mechanism ]
 .RB [ \-sasl " | " \-nosasl ]
 .RB [ \-saslmech
 .IR mechanism ]
+.RB [ \-oauth
+.IR service ]
 .RB [ \-snoop ]
 .RB [ \-version ]
 .RB [ \-help ]
 .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
 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
 .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.
 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
 .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 scan (1),
 .IR mh\-mail (5),
 .IR mh\-profile (5),
+.IR mhlogin (1),
 .IR post (8),
 .IR rcvstore (1)
 .SH DEFAULTS
 .IR post (8),
 .IR rcvstore (1)
 .SH DEFAULTS
diff --git a/man/mhlogin.man b/man/mhlogin.man
new file mode 100644 (file)
index 0000000..0651fdc
--- /dev/null
@@ -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)
index 0d5d6c9873e608cad37706f815f341548de0c606..5b02c52ce1e510172429913cf670265c41c1323a 100644 (file)
@@ -1,4 +1,4 @@
-.TH MSGCHK %manext1% "April 14, 2013" "%nmhversion%"
+.TH MSGCHK %manext1% "November 25, 2014" "%nmhversion%"
 .\"
 .\" %nmhwarning%
 .\"
 .\"
 .\" %nmhwarning%
 .\"
@@ -20,6 +20,8 @@ all/mail/nomail ]
 .RB [ \-sasl ]
 .RB [ \-saslmech
 .IR mechanism ]
 .RB [ \-sasl ]
 .RB [ \-saslmech
 .IR mechanism ]
+.RB [ \-oauth
+.IR service ]
 .RB [ \-snoop ]
 .RI [ users
 \&... ]
 .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
 .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
 .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,
 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.
 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
 .SH FILES
 .fc ^ ~
 .nf
index 83333673d6a5ae9bf48455fd5f101b0144230e11..29e6e2431aca7a186c062d9d47d65a87ec5922fe 100644 (file)
@@ -1,7 +1,7 @@
 .\"
 .\" %nmhwarning%
 .\"
 .\"
 .\" %nmhwarning%
 .\"
-.TH SEND %manext1% "July 8, 2014" "%nmhversion%"
+.TH SEND %manext1% "November 25, 2014" "%nmhversion%"
 .SH NAME
 send \- send a message
 .SH SYNOPSIS
 .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 [ \-msgid " | " \-nomsgid ]
 .RB [ \-messageid
 .IR localname " | " random ]
+.RB [ \-oauth
+.IR service ]
 .RB [ \-push " | " \-nopush ]
 .RB [ \-split
 .IR seconds ]
 .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
 .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
 .PP
 If
 .B nmh
@@ -416,6 +420,24 @@ underlying SASL mechanism.  A value of 0 disables encryption.
 .PP
 If
 .B nmh
 .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
 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 forw (1),
 .IR mhbuild (1),
 .IR mhparam (1),
+.IR mhlogin (1),
 .IR repl (1),
 .IR whatnow (1),
 .IR mh\-alias (5),
 .IR repl (1),
 .IR whatnow (1),
 .IR mh\-alias (5),
index 873e0cd8d5c59400fb46b15f8b929fed4ab2a4b5..39cb713ab2d5ff3bba49cee68c239deaa2111e5a 100644 (file)
@@ -76,9 +76,7 @@
 #define        SM_DOT  600     /* see above */
 #define        SM_QUIT  30
 #define        SM_CLOS  10
 #define        SM_DOT  600     /* see above */
 #define        SM_QUIT  30
 #define        SM_CLOS  10
-#ifdef CYRUS_SASL
 #define        SM_AUTH  45
 #define        SM_AUTH  45
-#endif /* CYRUS_SASL */
 
 static int sm_addrs = 0;
 static int sm_alarmed = 0;
 
 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,
  * 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 *);
 
 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_fputc(int);
 static void sm_fflush(void);
 static int sm_fgets(char *, int, FILE *);
+static int sm_auth_xoauth2(const char *);
 
 #ifdef CYRUS_SASL
 /*
 
 #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
 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,
 {
     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);
     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,
 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;
 {
     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);
     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) {
      */
 
     if (sasl) {
+        char *server_mechs;
        if (! (server_mechs = EHLOset("AUTH"))) {
            sm_end(NOTOK);
            return sm_ierror("SMTP server does not support SASL");
        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 */
 
     }
 #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");
 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 */
 
 }
 #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, ...)
 {
 static int
 sm_ierror (char *fmt, ...)
 {
index 72caaccbee0de559d4f7e0cb524715e6239498c4..268de50a7ef92dc3c78830a61020838194beefb8 100644 (file)
@@ -16,7 +16,8 @@ struct smtp {
  * prototypes
  */
 /* int client (); */
  * 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);
 int sm_winit (char *);
 int sm_wadr (char *, char *, char *);
 int sm_waend (void);
index 0b6d77774589def21f2552cae634e014b0873ae7..184c950933b1e79661e40f53bec4c261d78518a2 100644 (file)
@@ -31,7 +31,7 @@ advise (char *what, char *fmt, ...)
  * print out error message and exit
  */
 void
  * print out error message and exit
  */
 void
-adios (char *what, char *fmt, ...)
+adios (char *what, const char *fmt, ...)
 {
     va_list ap;
 
 {
     va_list ap;
 
@@ -60,7 +60,7 @@ admonish (char *what, char *fmt, ...)
  * main routine for printing error messages.
  */
 void
  * 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];
 {
     int        eindex = errno;
     char buffer[BUFSIZ], err[BUFSIZ];
diff --git a/sbr/oauth.c b/sbr/oauth.c
new file mode 100644 (file)
index 0000000..6c284d8
--- /dev/null
@@ -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 <h/mh.h>
+
+#ifdef OAUTH_SUPPORT
+
+#include <sys/stat.h>
+
+#include <stdarg.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <strings.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <curl/curl.h>
+#include <thirdparty/jsmn/jsmn.h>
+
+#include <h/oauth.h>
+#include <h/utils.h>
+
+#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
index 2e319fe6b265a02ec06b0ed6195e021ca70645ec..5d1168939d05cb65ac8dd10483b413c601acf557 100644 (file)
@@ -42,7 +42,7 @@ static struct node **opp = NULL;
 
 
 void
 
 
 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;
 {
     register int state;
     register char *cp;
diff --git a/test/fakehttp.c b/test/fakehttp.c
new file mode 100644 (file)
index 0000000..f773a73
--- /dev/null
@@ -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 <errno.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#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;
+}
index 4d023545a43b0a54cbdfe888e834f19a9c860499..bc2baae0c399e32f01e4dd1766d36a882db1992d 100644 (file)
 #include <stdlib.h>
 #include <string.h>
 #include <unistd.h>
 #include <stdlib.h>
 #include <string.h>
 #include <unistd.h>
-#include <netdb.h>
 #include <errno.h>
 #include <errno.h>
-#include <sys/socket.h>
-#include <netinet/in.h>
 #include <sys/types.h>
 #include <sys/types.h>
-#include <sys/select.h>
-#include <sys/stat.h>
-#include <sys/uio.h>
 #include <limits.h>
 #include <limits.h>
-#include <signal.h>
 
 #define PIDFILE "/tmp/fakepop.pid"
 #define LINESIZE 1024
 #define BUFALLOC 4096
 
 #define CHECKUSER()    if (!user) { \
 
 #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; \
                        }
                                       "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; \
                        }
 
                                       "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 *);
 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[])
 {
 int
 main(int argc, char *argv[])
 {
-       struct addrinfo hints, *res;
-       struct stat st;
-       FILE **mfiles, *pid;
+       FILE **mfiles;
        char line[LINESIZE];
        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;
        int numfiles;
        size_t *octets;
+       const char *xoauth;
 
        if (argc < 5) {
                fprintf(stderr, "Usage: %s port username "
 
        if (argc < 5) {
                fprintf(stderr, "Usage: %s port username "
@@ -64,6 +52,12 @@ main(int argc, char *argv[])
                exit(1);
        }
 
                exit(1);
        }
 
+       if (strcmp(argv[2], "XOAUTH") == 0) {
+               xoauth = argv[3];
+       } else {
+               xoauth = NULL;
+       }
+
        numfiles = argc - 4;
 
        mfiles = malloc(sizeof(FILE *) * numfiles);
        numfiles = argc - 4;
 
        mfiles = malloc(sizeof(FILE *) * numfiles);
@@ -105,153 +99,13 @@ main(int argc, char *argv[])
                rewind(mfiles[j]);
        }
 
                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
         */
 
 
        /*
         * 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];
 
        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"
 
                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) {
                } 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 {
                                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) {
                        }
                } 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!");
                                       "for sore eyes!");
-                               pass = 1;
+                               auth = 1;
                        } else {
                        } 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;
                } 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];
                        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);
                        }
                        snprintf(linebuf, sizeof(linebuf),
                                         "+OK %d %d", i, (int) total);
-                       putpop(s, linebuf);
+                       putcrlf(s, linebuf);
                } else if (strncasecmp(linebuf, "RETR ", 5) == 0) {
                } else if (strncasecmp(linebuf, "RETR ", 5) == 0) {
-                       CHECKUSERPASS();
+                       CHECKAUTH();
                        rc = sscanf(linebuf + 5, "%d", &i);
                        if (rc != 1) {
                        rc = sscanf(linebuf + 5, "%d", &i);
                        if (rc != 1) {
-                               putpop(s, "-ERR Whaaaa...?");
+                               putcrlf(s, "-ERR Whaaaa...?");
                                continue;
                        }
                        if (i < 1 || i > numfiles) {
                                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) {
                                       "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]);
                        } 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) {
                                putpopbulk(s, buf);
                                free(buf);
                        }
                } else if (strncasecmp(linebuf, "DELE ", 5) == 0) {
-                       CHECKUSERPASS();
+                       CHECKAUTH();
                        rc = sscanf(linebuf + 5, "%d", &i);
                        if (rc != 1) {
                        rc = sscanf(linebuf + 5, "%d", &i);
                        if (rc != 1) {
-                               putpop(s, "-ERR Whaaaa...?");
+                               putcrlf(s, "-ERR Whaaaa...?");
                                continue;
                        }
                        if (i < 1 || i > numfiles) {
                                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) {
                                       "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;
                                       "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) {
                        }
                } 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 {
                        close(s);
                        break;
                } else {
-                       putpop(s, "-ERR Um, what?");
+                       putcrlf(s, "-ERR Um, what?");
                }
        }
 
        exit(0);
 }
 
                }
        }
 
        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.
 /*
  * 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;
 }
 
        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);
-}
index b812f23e2e24682ba516da1ea9d207500f0aff1f..42d4f181ded9b31a422bc5192a7c063601e60383 100644 (file)
 #include <stdlib.h>
 #include <string.h>
 #include <unistd.h>
 #include <stdlib.h>
 #include <string.h>
 #include <unistd.h>
-#include <netdb.h>
 #include <errno.h>
 #include <sys/socket.h>
 #include <errno.h>
 #include <sys/socket.h>
-#include <netinet/in.h>
 #include <sys/types.h>
 #include <sys/types.h>
-#include <sys/select.h>
 #include <sys/stat.h>
 #include <sys/stat.h>
-#include <sys/uio.h>
-#include <signal.h>
 
 #define PIDFILE "/tmp/fakesmtp.pid"
 
 #define LINESIZE 1024
 
 
 #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[])
 {
 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]);
 
        if (argc != 3) {
                fprintf(stderr, "Usage: %s output-filename port\n", argv[0]);
@@ -51,156 +53,14 @@ main(int argc, char *argv[])
                exit(1);
        }
 
                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.
         */
 
        /*
         * 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];
 
        for (;;) {
                char line[LINESIZE];
@@ -212,17 +72,17 @@ main(int argc, char *argv[])
 
                fprintf(f, "%s\n", line);
 
 
                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) {
                        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;
                        }
                        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;
                if (strcmp(line, "QUIT") == 0) {
                        fclose(f);
                        f = NULL;
-                       putsmtp(conn, "221 Later alligator!");
+                       putcrlf(conn, "221 Later alligator!");
                        close(conn);
                        break;
                        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)
        }
 
        if (f)
@@ -249,25 +128,6 @@ main(int argc, char *argv[])
        exit(0);
 }
 
        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)
  */
 /*
  * Read a line (up to the \r\n)
  */
@@ -322,27 +182,3 @@ getsmtp(int socket, char *data)
                bytesinbuf += cc;
        }
 }
                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 (file)
index 0000000..5deebfd
--- /dev/null
@@ -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} <<EOF
+oauth-test-scope: test-scope
+oauth-test-client_id: test-id
+oauth-test-client_secret: test-secret
+oauth-test-auth_endpoint: http://127.0.0.1:${http_port}/oauth/auth
+oauth-test-token_endpoint: http://127.0.0.1:${http_port}/oauth/token
+oauth-test-redirect_uri: urn:ietf:wg:oauth:2.0:oob
+EOF
+
+setup_pop() {
+    pop_message=${MH_TEST_DIR}/testmessage
+    cat > "${pop_message}" <<EOM
+Received: From somewhere
+From: No Such User <nosuch@example.com>
+To: Some Other User <someother@example.com>
+Subject: Hello
+Date: Sun, 17 Dec 2006 12:13:14 -0500
+
+Hey man
+EOM
+}
+
+setup_draft() {
+    cat > "${MH_TEST_DIR}/Mail/draft" <<EOF
+From: Mr Nobody <nobody@example.com>
+To: Somebody Else <somebody@example.com>
+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" <<EOF
+POST /oauth/token HTTP/1.1\r
+User-Agent: nmh/${MH_VERSION} ${CURL_USER_AGENT}\r
+Host: 127.0.0.1:${http_port}\r
+Accept: */*\r
+Content-Length: $1\r
+Content-Type: application/x-www-form-urlencoded\r
+\r
+$2
+EOF
+}
+
+expect_http_post_code() {
+    expect_http_post 132 'code=code&grant_type=authorization_code&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&client_id=test-id&client_secret=test-secret'
+}
+
+expect_http_post_refresh() {
+    expect_http_post 95 'grant_type=refresh_token&refresh_token=test-refresh&client_id=test-id&client_secret=test-secret'
+}
+
+expect_http_post_old_refresh() {
+    expect_http_post 94 'grant_type=refresh_token&refresh_token=old-refresh&client_id=test-id&client_secret=test-secret'
+}
+
+expect_creds() {
+    cat > "${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<<Hey man >>'
+    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 (executable)
index 0000000..8c71e62
--- /dev/null
@@ -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 <<EOF
+access: test-access
+expire: 2000000000
+EOF
+
+start_pop_xoauth
+
+test_inc_success
+
+# TEST
+echo 'expired access token, refresh works, pop server accepts message'
+
+fake_creds <<EOF
+access: old-access
+refresh: test-refresh
+expire: 1414303986
+EOF
+
+expect_http_post_refresh
+
+fake_json_response <<EOF
+{
+  "access_token": "test-access",
+  "token_type": "Bearer",
+  "expires_in": 3600
+}
+EOF
+
+expect_creds <<EOF
+access: test-access
+refresh: test-refresh
+expire:
+EOF
+
+start_fakehttp
+start_pop_xoauth
+
+test_inc_success
+
+check_http_req
+
+#
+# fail cases
+#
+
+# TEST
+echo 'refresh gets proper error from http'
+
+fake_creds <<EOF
+access: test-access
+refresh: test-refresh
+EOF
+
+expect_http_post_refresh
+
+fake_http_response '400 Bad Request' <<EOF
+Content-Type: application/json
+
+{
+  "error": "invalid_request"
+}
+EOF
+
+start_fakehttp
+
+test_inc 'inc: error refreshing OAuth2 token
+inc: bad OAuth request; re-run with -snoop and send REDACTED output to nmh-workers'
+
+check_http_req
+
+# TEST
+echo 'pop server rejects token'
+
+fake_creds <<EOF
+access: wrong-access
+expire: 2000000000
+EOF
+
+start_pop_xoauth
+
+test_inc 'inc: -ERR [AUTH] Invalid credentials.'
+
+# TEST
+echo "pop server doesn't support oauth"
+
+fake_creds <<EOF
+access: test-access
+expire: 2000000000
+EOF
+
+start_pop testuser testpass
+
+test_inc 'inc: POP server does not support SASL'
+
+exit ${failed:-0}
diff --git a/test/oauth/test-mhlogin b/test/oauth/test-mhlogin
new file mode 100755 (executable)
index 0000000..6103f70
--- /dev/null
@@ -0,0 +1,257 @@
+#!/bin/sh
+#
+# Test mhlogin
+#
+
+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"
+
+expect_no_creds() {
+    cat /dev/null > "${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 <<EOF
+{
+  "access_token": "test-access",
+  "token_type": "Bearer",
+  "expires_in": 3600
+}
+EOF
+
+expect_creds <<EOF
+access: test-access
+expire:
+EOF
+
+test_mhlogin
+
+# TEST
+echo 'mhlogin receives access and refresh'
+
+expect_http_post_code
+
+fake_json_response <<EOF
+{
+  "access_token": "test-access",
+  "token_type": "Bearer"
+}
+EOF
+
+expect_creds <<EOF
+access: test-access
+EOF
+
+test_mhlogin
+
+# TEST
+echo 'mhlogin receives access, expiration, and refresh'
+
+expect_http_post_code
+
+fake_json_response <<EOF
+{
+  "access_token": "test-access",
+  "refresh_token": "refresh-token",
+  "expires_in": 3600,
+  "token_type": "Bearer"
+}
+EOF
+
+expect_creds <<EOF
+access: test-access
+refresh: refresh-token
+expire:
+EOF
+
+test_mhlogin
+
+# TEST
+echo 'mhlogin receives refresh only'
+
+expect_http_post_code
+
+fake_json_response <<EOF
+{
+  "refresh_token": "refresh-token",
+  "token_type": "Bearer"
+}
+EOF
+
+expect_creds <<EOF
+refresh: refresh-token
+EOF
+
+test_mhlogin
+
+# TEST
+echo 'mhlogin receives token_type only'
+
+expect_http_post_code
+
+fake_json_response <<EOF
+{
+  "token_type": "Bearer"
+}
+EOF
+
+expect_no_creds
+
+test_mhlogin_invalid_response
+
+# TEST
+echo 'mhlogin ignores extra bits in successful response JSON'
+
+expect_http_post_code
+
+fake_json_response <<EOF
+{
+  "access_token": "test-access",
+  "refresh_token": "refresh-token",
+  "extra_object": {
+    "a": 1,
+    "b": [1, 2, 3],
+    "c": [{}, {"foo": "bar"}]
+  },
+  "extra_int": 1,
+  "expires_in": 3600,
+  "token_type": "Bearer"
+}
+EOF
+
+expect_creds <<EOF
+access: test-access
+refresh: refresh-token
+expire:
+EOF
+
+test_mhlogin
+
+# TEST
+echo 'mhlogin user enters bad code'
+
+expect_http_post_code
+
+fake_http_response '400 Bad Request' <<EOF
+Content-Type: application/json
+
+{
+  "error": "invalid_grant"
+}
+EOF
+
+expect_no_creds
+
+test_mhlogin 'Code rejected; try again? '
+
+#
+# fail cases
+#
+
+# TEST
+echo 'mhlogin response has no content-type'
+
+expect_http_post_code
+
+fake_http_response '200 OK' <<EOF
+
+{
+  "access_token": "test-access",
+  "token_type": "Bearer",
+  "expires_in": 3600
+}
+EOF
+
+expect_no_creds
+
+test_mhlogin_invalid_response
+
+# TEST
+echo 'mhlogin JSON array'
+
+expect_http_post_code
+
+fake_json_response <<EOF
+[]
+EOF
+
+expect_no_creds
+
+test_mhlogin_invalid_response
+
+# TEST
+echo 'mhlogin JSON empty object'
+
+expect_http_post_code
+
+fake_json_response <<EOF
+{}
+EOF
+
+expect_no_creds
+
+test_mhlogin_invalid_response
+
+# TEST
+echo 'mhlogin empty response body'
+
+expect_http_post_code
+
+fake_json_response <<EOF
+EOF
+
+expect_no_creds
+
+test_mhlogin_invalid_response
+
+# TEST
+echo 'mhlogin gets proper error from http'
+
+expect_http_post_code
+
+fake_http_response '400 Bad Request' <<EOF
+Content-Type: application/json
+
+{
+  "error": "invalid_request"
+}
+EOF
+
+expect_no_creds
+
+test_mhlogin 'mhlogin: error exchanging code for OAuth2 token
+mhlogin: bad OAuth request; re-run with -snoop and send REDACTED output to nmh-workers'
+
+exit ${failed:-0}
diff --git a/test/oauth/test-send b/test/oauth/test-send
new file mode 100755 (executable)
index 0000000..513c898
--- /dev/null
@@ -0,0 +1,358 @@
+#!/bin/sh
+#
+# Test the XOAUTH2 support in sen
+#
+
+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"
+
+test_send_only_fakehttp() {
+    start_fakehttp
+    test_send_no_servers "$@"
+    check_http_req
+}
+
+#
+# success cases
+#
+
+export XOAUTH
+XOAUTH='dXNlcj1ub2JvZHlAZXhhbXBsZS5jb20BYXV0aD1CZWFyZXIgdGVzdC1hY2Nlc3MBAQ=='
+
+# TEST
+echo 'access token ready, smtp server accepts message'
+
+setup_draft
+
+fake_creds <<EOF
+access: test-access
+refresh: test-refresh
+expire: 2000000000
+EOF
+
+start_fakesmtp
+run_test "send -draft -server 127.0.0.1 -port ${smtp_port} -oauth test -user nobody@example.com"
+
+# TEST
+echo 'expired access token, refresh works, smtp server accepts message'
+
+setup_draft
+
+fake_creds <<EOF
+access: old-access
+refresh: test-refresh
+expire: 1414303986
+EOF
+
+expect_http_post_refresh
+
+fake_json_response <<EOF
+{
+  "access_token": "test-access",
+  "token_type": "Bearer",
+  "expires_in": 3600
+}
+EOF
+
+expect_creds <<EOF
+access: test-access
+refresh: test-refresh
+expire:
+EOF
+
+test_send
+
+check_creds_private
+check_creds
+
+# TEST
+echo 'expired access token, refresh works and gets updated, smtp server accepts message'
+
+setup_draft
+
+fake_creds <<EOF
+access: old-access
+refresh: old-refresh
+expire: 1414303986
+EOF
+
+expect_http_post_old_refresh
+
+fake_json_response <<EOF
+{
+  "access_token": "test-access",
+  "refresh_token": "test-refresh",
+  "token_type": "Bearer"
+}
+EOF
+
+expect_creds <<EOF
+access: test-access
+refresh: test-refresh
+EOF
+
+test_send
+
+check_creds
+
+# TEST
+echo 'access token has no expiration, refresh works, smtp server accepts message'
+
+setup_draft
+
+fake_creds <<EOF
+access: old-access
+refresh: test-refresh
+EOF
+
+expect_http_post_refresh
+
+fake_json_response <<EOF
+{
+  "access_token": "test-access",
+  "token_type": "Bearer"
+}
+EOF
+
+expect_creds <<EOF
+access: test-access
+refresh: test-refresh
+EOF
+
+test_send
+
+check_creds
+
+# TEST
+echo 'no access token, refresh works, smtp server accepts message'
+
+setup_draft
+
+fake_creds <<EOF
+refresh: test-refresh
+EOF
+
+expect_http_post_refresh
+
+fake_json_response <<EOF
+{
+  "access_token": "test-access",
+  "token_type": "Bearer"
+}
+EOF
+
+expect_creds <<EOF
+access: test-access
+refresh: test-refresh
+EOF
+
+test_send
+
+check_creds
+
+#
+# fail cases
+#
+
+setup_draft
+
+# TEST
+echo 'no service definition'
+
+run_test "send -draft -server 127.0.0.1 -port ${smtp_port} -oauth bogus -user nobody@example.com" 'send: incomplete OAuth2 service definition: scope is missing'
+
+# TEST
+echo 'no creds file -- should tell user to mhlogin'
+
+rm -f "${MHTMPDIR}/oauth-test"
+
+test_send_no_servers 'send: no credentials -- run mhlogin -oauth test'
+
+# TEST
+echo 'empty creds file -- should tell user to mhlogin'
+
+fake_creds < /dev/null
+
+test_send_no_servers 'send: no valid credentials -- run mhlogin -oauth test'
+
+# TEST
+echo 'garbage creds file'
+
+echo bork | fake_creds
+
+test_send_no_servers 'send: eof encountered in field "bork"
+send: error loading cred file'
+
+# TEST
+echo 'unexpected field in creds file'
+
+fake_creds <<EOF
+bork: bork
+access: test-access
+EOF
+
+test_send_no_servers 'send: error loading cred file: unexpected field'
+
+# TEST
+echo 'garbage expiration time'
+
+fake_creds <<EOF
+access: test-access
+expire: 99999999999999999999999999999999
+EOF
+
+test_send_no_servers 'send: error loading cred file: invalid expiration time'
+
+# TEST
+echo 'refresh response has no access token'
+
+fake_creds <<EOF
+refresh: test-refresh
+EOF
+
+expect_http_post_refresh
+
+fake_json_response <<EOF
+{
+  "refresh_token": "refresh-token",
+  "token_type": "Bearer"
+}
+EOF
+
+test_send_only_fakehttp 'send: error refreshing OAuth2 token
+send: invalid response: no access token'
+
+# TEST
+echo 'expired access token, no refresh token -- tell user to mhlogin'
+
+fake_creds <<EOF
+access: test-access
+expire: 1414303986
+EOF
+
+test_send_no_servers 'send: no valid credentials -- run mhlogin -oauth test'
+
+# TEST
+echo 'access token has no expiration, no refresh token -- tell user to mhlogin'
+
+fake_creds <<EOF
+access: test-access
+EOF
+
+test_send_no_servers 'send: no valid credentials -- run mhlogin -oauth test'
+
+# TEST
+echo 'refresh finds no http server'
+
+fake_creds <<EOF
+access: test-access
+refresh: test-refresh
+EOF
+
+cat > "${testname}.expected-send-output" <<EOF
+send: error refreshing OAuth2 token
+send: error making HTTP request to OAuth2 authorization endpoint: [details]
+EOF
+
+run_prog send -draft -server 127.0.0.1 -port ${smtp_port} \
+  -oauth test -user nobody@example.com > "${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' <<EOF
+Content-Type: text/html
+
+<html>doh!</htmxl>
+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' <<EOF
+Content-Type: text/html
+
+<html>doh!</html>
+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' <<EOF
+Content-Type: application/json
+
+{
+  "error": "invalid_grant"
+}
+EOF
+
+test_send_only_fakehttp 'send: credentials rejected -- run mhlogin -oath test'
+
+# TEST
+echo 'refresh gets response too big'
+
+fake_creds <<EOF
+refresh: test-refresh
+EOF
+
+expect_http_post_refresh
+
+fake_json_response <<EOF
+{
+  "access_token": "test-access",
+  "token_type": "Bearer",
+  "expires_in": 3600
+}
+EOF
+
+awk 'BEGIN { for (i = 0; i < 8192; i++) { print "." } }' \
+    >> "${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 <<EOF
+access: test-access
+expire: 2000000000
+EOF
+
+test_send_only_fakesmtp 'post: problem initializing server; [BHST] Not no way, not no how!
+send: message not delivered to anyone'
+
+# TEST
+echo "smtp server doesn't support oauth"
+
+unset XOAUTH
+
+test_send_only_fakesmtp 'post: problem initializing server; [BHST] SMTP server does not support SASL XOAUTH2
+send: message not delivered to anyone'
+
+exit ${failed:-0}
diff --git a/test/oauth/test-share b/test/oauth/test-share
new file mode 100755 (executable)
index 0000000..af88756
--- /dev/null
@@ -0,0 +1,139 @@
+#!/bin/sh
+#
+# Test that inc, msgchck, and send share tokens.
+#
+
+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
+
+export XOAUTH
+XOAUTH='dXNlcj1ub2JvZHlAZXhhbXBsZS5jb20BYXV0aD1CZWFyZXIgdGVzdC1hY2Nlc3MBAQ=='
+
+# TEST
+echo 'mhlogin then all run with no refresh'
+
+expect_http_post_code
+
+fake_json_response <<EOF
+{
+  "access_token": "test-access",
+  "token_type": "Bearer",
+  "expires_in": 3600
+}
+EOF
+
+expect_creds <<EOF
+access: test-access
+expire:
+EOF
+
+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: "
+
+start_pop_xoauth
+run_test "msgchk -host 127.0.0.1 -port ${pop_port} -oauth test -user nobody@example.com" 'nobody@example.com has 1 message (178 bytes) on 127.0.0.1'
+
+start_pop_xoauth
+test_inc_success
+
+setup_draft
+test_send_only_fakesmtp
+
+# TEST
+echo 'inc refreshes'
+
+fake_creds <<EOF
+access: old-access
+refresh: test-refresh
+expire: 1414303986
+EOF
+
+expect_http_post_refresh
+
+fake_json_response <<EOF
+{
+  "access_token": "test-access",
+  "token_type": "Bearer",
+  "expires_in": 3600
+}
+EOF
+
+start_fakehttp
+start_pop_xoauth
+test_inc_success
+
+start_pop_xoauth
+run_test "msgchk -host 127.0.0.1 -port ${pop_port} -oauth test -user nobody@example.com" 'nobody@example.com has 1 message (178 bytes) on 127.0.0.1'
+
+setup_draft
+test_send_only_fakesmtp
+
+# TEST
+echo 'msgchck refreshes'
+
+fake_creds <<EOF
+access: old-access
+refresh: test-refresh
+expire: 1414303986
+EOF
+
+expect_http_post_refresh
+
+fake_json_response <<EOF
+{
+  "access_token": "test-access",
+  "token_type": "Bearer",
+  "expires_in": 3600
+}
+EOF
+
+start_fakehttp
+start_pop_xoauth
+run_test "msgchk -host 127.0.0.1 -port ${pop_port} -oauth test -user nobody@example.com" 'nobody@example.com has 1 message (178 bytes) on 127.0.0.1'
+
+start_pop_xoauth
+test_inc_success
+
+setup_draft
+test_send_only_fakesmtp
+
+# TEST
+echo 'send refreshes'
+
+fake_creds <<EOF
+access: old-access
+refresh: test-refresh
+expire: 1414303986
+EOF
+
+expect_http_post_refresh
+
+fake_json_response <<EOF
+{
+  "access_token": "test-access",
+  "token_type": "Bearer",
+  "expires_in": 3600
+}
+EOF
+
+setup_draft
+test_send
+
+start_pop_xoauth
+run_test "msgchk -host 127.0.0.1 -port ${pop_port} -oauth test -user nobody@example.com" 'nobody@example.com has 1 message (178 bytes) on 127.0.0.1'
+
+start_pop_xoauth
+test_inc_success
+
+exit ${failed:-0}
diff --git a/test/server.c b/test/server.c
new file mode 100644 (file)
index 0000000..13f3c87
--- /dev/null
@@ -0,0 +1,248 @@
+/*
+ * server.c - Utilities for fake servers 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 <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <netdb.h>
+#include <errno.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <sys/types.h>
+#include <sys/select.h>
+#include <sys/stat.h>
+#include <sys/uio.h>
+#include <signal.h>
+
+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);
+       }
+}
index 8b9fe7ae876e3a52ca4f85b813e1b5b1033f0c61..a4aea3e7d38412ae029fb2e37497b893d6a40bbb 100644 (file)
--- 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("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) \
     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 */
     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;
 
     int nmsgs, nbytes;
     char *MAILHOST_env_variable;
-
     done=inc_done;
 
 /* absolutely the first thing we do is save our privileges,
     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;
 
                    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]);
            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 (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
         */
        /*
         * initialize POP connection
         */
-       nmh_get_credentials (host, user, sasl, &creds);
        if (pop_init (host, port, creds.user, creds.password, proxy, snoop,
        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 */
            adios (NULL, "%s", response);
 
        /* Check if there are any messages */
diff --git a/uip/mhlogin.c b/uip/mhlogin.c
new file mode 100644 (file)
index 0000000..4fa10e1
--- /dev/null
@@ -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 <stdio.h>
+#include <string.h>
+
+#include <h/mh.h>
+#include <h/oauth.h>
+
+#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
+}
index 49e758f07be9aa4c37a1dca644df06b624ed4324..ef9d32b6238d669b249b2b6736763e12133ea23d 100644 (file)
@@ -33,6 +33,7 @@
     X("snoop", -5, SNOOPSW) \
     X("sasl", SASLminc(-4), SASLSW) \
     X("saslmech", SASLminc(-5), SASLMECHSW) \
     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,
     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,
 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
 
 
 int
@@ -84,6 +85,7 @@ main (int argc, char **argv)
     char buf[BUFSIZ], *saslmech = NULL; 
     char **argp, **arguments, *vec[MAXVEC];
     struct passwd *pw;
     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; }
 
 
     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;
 
                        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]);
                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,
     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,
        } 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 ();
        }
     } 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,
 
 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 };
 
 {
     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 */
     /* open the POP connection */
-    nmh_get_credentials (host, user, sasl, &creds);
     if (pop_init (host, port, creds.user, creds.password, proxy, snoop, sasl,
     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);
            || pop_stat (&nmsgs, &nbytes) == NOTOK     /* check for messages  */
            || pop_quit () == NOTOK) {                 /* quit POP connection */
        advise (NULL, "%s", response);
index ccc76c7c40ead4ba3eb72196f1e6314889928fb6..5b99c8cf75a3883ff6779925feea1327c9d0e0da 100644 (file)
@@ -8,6 +8,7 @@
 
 #include <h/mh.h>
 #include <h/utils.h>
 
 #include <h/mh.h>
 #include <h/utils.h>
+#include <h/oauth.h>
 
 #ifdef CYRUS_SASL
 # include <sasl/sasl.h>
 
 #ifdef CYRUS_SASL
 # include <sasl/sasl.h>
@@ -80,34 +81,10 @@ static int sasl_getline (char *, int, FILE *);
 static int putline (char *, 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
 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
 
     /*
      * 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++;
                 * 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;
        }
            }
            break;
        }
@@ -159,6 +136,42 @@ pop_auth_sasl(char *user, char *host, char *mech)
        return NOTOK;
     }
 
        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.
      */
     /*
      * 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 */
 
 }
 #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
 
 /*
  * 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
 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];
 {
     int fd1, fd2;
     char buffer[BUFSIZ];
+    const char *xoauth_client_res = NULL;
 #ifndef CYRUS_SASL
     NMH_UNUSED (sasl);
     NMH_UNUSED (mech);
 #endif /* ! CYRUS_SASL */
 
 #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 */
     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 */
                        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)
                if (command ("USER %s", user) != NOTOK
                    && command ("%s %s", (pophack++, "PASS"),
                                        pass) != NOTOK)
index 685129b9653de6fbbea99aea693d5606644df3e4..0beab55beafb79b24376c8cc8cfa4ee87a869b94 100644 (file)
@@ -80,6 +80,7 @@
     X("nosasl", SASLminc(-6), NOSASLSW) \
     X("saslmaxssf", SASLminc(-10), SASLMXSSFSW) \
     X("saslmech", SASLminc(-5), SASLMECHSW) \
     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) \
     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 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 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);
 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 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; }
 
 
     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;
                    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]);
                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. */
     /* 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);
     }
 
        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 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 {
        (void) m_unlink (bccfil);
     } else {
-       post (tmpfil, 0, isatty (1), envelope);
+       post (tmpfil, 0, isatty (1), envelope, xoauth_client_res);
     }
 
     p_refile (tmpfil);
     }
 
     p_refile (tmpfil);
@@ -1486,7 +1494,8 @@ do_addresses (int bccque, int talk)
  */
 
 static void
  */
 
 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;
 {
     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,
     } 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);
            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
 /* 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;
 {
     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,
     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));
 
                || rp_isbad (retval = sm_winit (envelope)))
            die (NULL, "problem initializing server; %s", rp_string (retval));
 
index 5885c98e71560d6371aea9a93d6faeb6f430092a..4c022b4cac6624da160ece7685990c6d7b4d2b61 100644 (file)
@@ -10,6 +10,8 @@
 #include <h/mh.h>
 #include <fcntl.h>
 
 #include <h/mh.h>
 #include <fcntl.h>
 
+#include <h/oauth.h>
+#include <h/utils.h>
 
 #ifndef CYRUS_SASL
 # define SASLminc(a) (a)
 
 #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("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) \
     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;
     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;
     struct msgs *mp;
     struct stat st;
+    int snoop = 0;
 
     if (nmh_init(argv[0], 1)) { return 1; }
 
 
     if (nmh_init(argv[0], 1)) { return 1; }
 
@@ -227,6 +232,11 @@ main (int argc, char **argv)
                    vec[vecp++] = --cp;
                    continue;
 
                    vec[vecp++] = --cp;
                    continue;
 
+               case SNOOPSW:
+                    snoop++;
+                   vec[vecp++] = --cp;
+                   continue;
+
                case DEBUGSW: 
                    debugsw++;  /* fall */
                case NFILTSW: 
                case DEBUGSW: 
                    debugsw++;  /* fall */
                case NFILTSW: 
@@ -238,7 +248,6 @@ main (int argc, char **argv)
                case NMSGDSW: 
                case WATCSW: 
                case NWATCSW: 
                case NMSGDSW: 
                case WATCSW: 
                case NWATCSW: 
-               case SNOOPSW: 
                case SASLSW:
                case NOSASLSW:
                case TLSSW:
                case SASLSW:
                case NOSASLSW:
                case TLSSW:
@@ -247,6 +256,25 @@ main (int argc, char **argv)
                    vec[vecp++] = --cp;
                    continue;
 
                    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: 
                case ALIASW: 
                case FILTSW: 
                case WIDTHSW: 
@@ -254,7 +282,6 @@ main (int argc, char **argv)
                case SERVSW: 
                case SASLMECHSW:
                case SASLMXSSFSW:
                case SERVSW: 
                case SASLMECHSW:
                case SASLMXSSFSW:
-               case USERSW:
                case PORTSW:
                case MTSSW:
                case MESSAGEIDSW:
                case PORTSW:
                case MTSSW:
                case MESSAGEIDSW:
@@ -416,6 +443,18 @@ go_to_it:
        distfile = NULL;
     }
 
        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;
     if (altmsg == NULL || stat (altmsg, &st) == NOTOK) {
        st.st_mtime = 0;
        st.st_dev = 0;