/uip/mhical
/uip/mhl
/uip/mhlist
+/uip/mhlogin
/uip/mhn
/uip/mhparam
/uip/mhpath
/uip/whatnow
/uip/whom
/uip/*.exe
+/test/fakehttp
/test/fakepop
/test/fakesmtp
/test/getcanon
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
OF SUCH DAMAGE.
+
+********************************************************************************
+
+Contains a copy of Serge A. Zaitsev's jsmn library in thirdparty/jsmn,
+whose LICENSE file states:
+
+Copyright (c) 2010 Serge A. Zaitsev
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
readline (if you want readline support)
cyrus-sasl-lib / libsasl2 (if configured with --with-cyrus-sasl)
openssl-libs / libssl (if configured with --with-tls)
+ libcurl (if configured with --with-oauth)
Additional build-time package requirements:
ncurses-devel / libncurses5-devel
readline-devel (if you want readline support)
cyrus-sasl-devel / libsasl2-dev (if configuring with --with-cyrus-sasl)
openssl-devel / libssl-dev (if configuring with --with-tls)
+ libcurl-devel / libcurl-dev (if configured with --with-oauth)
Test suite package requirements:
ncurses (tput is needed to run test/format/test-curses)
Additional build-time package requirements:
devel/autotools
+ ftp/curl (if configured with --with-oauth)
------------------------------------------------------------------------------
Cygwin
## 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)" \
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 \
check_SCRIPTS = test/common.sh
check_PROGRAMS = test/getfullname test/getcanon test/fakepop test/fakesmtp \
- test/getcwidth
+ test/getcwidth test/fakehttp
## The location of installed nmhetcdir is, for all purposes except
## distcheck, $nmhetcdir. For distcheck, prepend $MH_INST_DIR (from
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
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
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
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
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)
##
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_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_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_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)
test_getcanon_SOURCES = test/getcanon.c
test_getcanon_LDADD = $(POSTLINK)
-test_fakepop_SOURCES = test/fakepop.c
+test_fakepop_SOURCES = test/fakepop.c test/server.c
test_fakepop_LDADD = $(POSTLINK)
-test_fakesmtp_SOURCES = test/fakesmtp.c
+test_fakesmtp_SOURCES = test/fakesmtp.c test/server.c
test_fakesmtp_LDADD = $(POSTLINK)
+test_fakehttp_SOURCES = test/fakehttp.c test/server.c
+test_fakehttp_LDADD = $(POSTLINK)
+
test_getcwidth_SOURCES = test/getcwidth.c
test_getcwidth_LDADD = $(POSTLINK)
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,
echo "char *version_str = \"nmh-$VERSION [compiled on $HOSTNAME at `date`]\";"
fi
echo "char *version_num = \"nmh-$VERSION\";"
+echo "char *user_agent = \"nmh/$VERSION\";"
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"],[
[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 ----------------
default smtp servers : ${smtpservers}
SASL support : ${sasl_support}
TLS support : ${tls_support}
+OAuth support : ${oauth_support}
])])dnl
dnl ---------------
extern char *showmimeproc;
extern char *showproc;
extern char *usequence;
+extern char *user_agent;
extern char *version_num;
extern char *version_str;
extern char *whatnowproc;
--- /dev/null
+/*
+ * 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);
* 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 *));
struct msgs_array;
void add_profile_entry (const char *, const char *);
-void adios (char *, char *, ...) NORETURN;
+void adios (char *, const char *, ...) NORETURN;
void admonish (char *, char *, ...);
-void advertise (char *, char *, char *, va_list);
+void advertise (char *, char *, const char *, va_list);
void advise (char *, char *, ...);
char **argsplit (char *, char **, int *);
void argsplit_msgarg (struct msgs_array *, char *, char **);
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 *);
-.TH INC %manext1% "April 18, 2014" "%nmhversion%"
+.TH INC %manext1% "November 25, 2014" "%nmhversion%"
.\"
.\" %nmhwarning%
.\"
.RB [ \-sasl " | " \-nosasl ]
.RB [ \-saslmech
.IR mechanism ]
+.RB [ \-oauth
+.IR service ]
.RB [ \-snoop ]
.RB [ \-version ]
.RB [ \-help ]
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
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
.IR scan (1),
.IR mh\-mail (5),
.IR mh\-profile (5),
+.IR mhlogin (1),
.IR post (8),
.IR rcvstore (1)
.SH DEFAULTS
--- /dev/null
+.\"
+.\" %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)
-.TH MSGCHK %manext1% "April 14, 2013" "%nmhversion%"
+.TH MSGCHK %manext1% "November 25, 2014" "%nmhversion%"
.\"
.\" %nmhwarning%
.\"
.RB [ \-sasl ]
.RB [ \-saslmech
.IR mechanism ]
+.RB [ \-oauth
+.IR service ]
.RB [ \-snoop ]
.RI [ users
\&... ]
.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
switch can be used to select a particular SASL mechanism.
.PP
If SASL authentication is successful,
-.B inc
+.B msgchk
will attempt to negotiate
a security layer for session encryption. Encrypted traffic is labelled
with `(encrypted)' and `(decrypted)' when viewing the POP transaction
with the
.B \-snoop
switch.
+.PP
+If
+.B nmh
+has been compiled with OAuth support, the
+.B \-oauth
+switch will enable OAuth authentication. The
+.B \-user
+switch must be used, and the
+.I user-name
+must be an email address the user has for that service. Before using this,
+the user must authorize nmh by running
+.B mhlogin
+and grant authorization to that account. Only
+.B -oauth
+.I gmail
+is supported. See the
+.B mhlogin
+man page for more details.
+.PP
+Gmail only supports POP3 over TLS, but
+.B msgchk
+has no TLS support. To work around this, use something like
+.B -proxy 'openssl s_client -connect %h:995 -CAfile /etc/ssl/certs/ca-certificates.crt -quiet'
.SH FILES
.fc ^ ~
.nf
.\"
.\" %nmhwarning%
.\"
-.TH SEND %manext1% "July 8, 2014" "%nmhversion%"
+.TH SEND %manext1% "November 25, 2014" "%nmhversion%"
.SH NAME
send \- send a message
.SH SYNOPSIS
.RB [ \-msgid " | " \-nomsgid ]
.RB [ \-messageid
.IR localname " | " random ]
+.RB [ \-oauth
+.IR service ]
.RB [ \-push " | " \-nopush ]
.RB [ \-split
.IR seconds ]
.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
+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
.IR forw (1),
.IR mhbuild (1),
.IR mhparam (1),
+.IR mhlogin (1),
.IR repl (1),
.IR whatnow (1),
.IR mh\-alias (5),
#define SM_DOT 600 /* see above */
#define SM_QUIT 30
#define SM_CLOS 10
-#ifdef CYRUS_SASL
#define SM_AUTH 45
-#endif /* CYRUS_SASL */
static int sm_addrs = 0;
static int sm_alarmed = 0;
* 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 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
/*
int
sm_init (char *client, char *server, char *port, int watch, int verbose,
- int debug, int sasl, int saslssf, char *saslmech, char *user, int tls)
+ int debug, int sasl, int saslssf, char *saslmech, char *user,
+ const char *xoauth_client_res, int tls)
{
if (sm_mts == MTS_SMTP)
return smtp_init (client, server, port, watch, verbose,
- debug, sasl, saslssf, saslmech, user, tls);
+ debug, sasl, saslssf, saslmech, user,
+ xoauth_client_res, tls);
else
return sendmail_init (client, server, watch, verbose,
debug, sasl, saslssf, saslmech, user);
static int
smtp_init (char *client, char *server, char *port, int watch, int verbose,
int debug,
- int sasl, int saslssf, char *saslmech, char *user, int tls)
+ int sasl, int saslssf, char *saslmech, char *user,
+ const char *xoauth_client_res, int tls)
{
int result, sd1, sd2;
-#ifdef CYRUS_SASL
- char *server_mechs;
-#else /* CYRUS_SASL */
+#ifndef CYRUS_SASL
NMH_UNUSED (sasl);
NMH_UNUSED (saslssf);
NMH_UNUSED (saslmech);
*/
if (sasl) {
+ char *server_mechs;
if (! (server_mechs = EHLOset("AUTH"))) {
sm_end(NOTOK);
return sm_ierror("SMTP server does not support 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");
}
#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, ...)
{
* 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);
* print out error message and exit
*/
void
-adios (char *what, char *fmt, ...)
+adios (char *what, const char *fmt, ...)
{
va_list ap;
* 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];
--- /dev/null
+/*
+ * 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
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;
--- /dev/null
+/*
+ * 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
+strip_cr(char *buf, ssize_t *len)
+{
+ ssize_t src, dst;
+ for (src = dst = 0; src < *len; src++) {
+ buf[dst] = buf[src];
+ if (buf[src] != '\r') {
+ dst++;
+ }
+ }
+ *len -= src - dst;
+}
+
+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;
+ }
+ strip_cr(buf, &r);
+ 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;
+}
#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 <limits.h>
-#include <signal.h>
#define PIDFILE "/tmp/fakepop.pid"
#define LINESIZE 1024
#define BUFALLOC 4096
#define CHECKUSER() if (!user) { \
- putpop(s, "-ERR Aren't you forgetting " \
+ putcrlf(s, "-ERR Aren't you forgetting " \
"something? Like the USER command?"); \
continue; \
}
-#define CHECKUSERPASS() CHECKUSER() \
- if (! pass) { \
- putpop(s, "-ERR Um, hello? Forget to " \
+#define CHECKAUTH() if (!auth) { \
+ putcrlf(s, "-ERR Um, hello? Forget to " \
"log in?"); \
continue; \
}
-static void killpidfile(void);
-static void handleterm(int);
-static void putpop(int, char *);
+void putcrlf(int, char *);
+int serve(const char *, const char *);
+
static void putpopbulk(int, char *);
static int getpop(int, char *, ssize_t);
static char *readmessage(FILE *);
int
main(int argc, char *argv[])
{
- struct addrinfo hints, *res;
- struct stat st;
- FILE **mfiles, *pid;
+ FILE **mfiles;
char line[LINESIZE];
- fd_set readfd;
- struct timeval tv;
- pid_t child;
- int rc, l, s, on, user = 0, pass = 0, i, j;
+ int rc, s, user = 0, auth = 0, i, j;
int numfiles;
size_t *octets;
+ const char *xoauth;
if (argc < 5) {
fprintf(stderr, "Usage: %s port username "
exit(1);
}
+ if (strcmp(argv[2], "XOAUTH") == 0) {
+ xoauth = argv[3];
+ } else {
+ xoauth = NULL;
+ }
+
numfiles = argc - 4;
mfiles = malloc(sizeof(FILE *) * numfiles);
rewind(mfiles[j]);
}
- /*
- * If there is a pid file around, kill the previously running
- * fakepop process.
- */
-
- if (stat(PIDFILE, &st) == 0) {
- long oldpid;
-
- if (!(pid = fopen(PIDFILE, "r"))) {
- fprintf(stderr, "Cannot open " PIDFILE
- " (%s), continuing ...\n", strerror(errno));
- } else {
- rc = fscanf(pid, "%ld", &oldpid);
- fclose(pid);
-
- if (rc != 1) {
- fprintf(stderr, "Unable to parse pid in "
- PIDFILE ", continuing ...\n");
- } else {
- kill((pid_t) oldpid, SIGTERM);
- }
- }
-
- unlink(PIDFILE);
- }
-
- memset(&hints, 0, sizeof(hints));
-
- hints.ai_family = PF_INET;
- hints.ai_socktype = SOCK_STREAM;
- hints.ai_protocol = IPPROTO_TCP;
- hints.ai_flags = AI_PASSIVE;
-
- rc = getaddrinfo("127.0.0.1", argv[1], &hints, &res);
-
- if (rc) {
- fprintf(stderr, "Unable to resolve localhost/%s: %s\n",
- argv[1], gai_strerror(rc));
- exit(1);
- }
-
- l = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
-
- if (l == -1) {
- fprintf(stderr, "Unable to create listening socket: %s\n",
- strerror(errno));
- exit(1);
- }
-
- on = 1;
-
- if (setsockopt(l, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1) {
- fprintf(stderr, "Unable to set SO_REUSEADDR: %s\n",
- strerror(errno));
- exit(1);
- }
-
- if (bind(l, res->ai_addr, res->ai_addrlen) == -1) {
- fprintf(stderr, "Unable to bind socket: %s\n", strerror(errno));
- exit(1);
- }
-
- if (listen(l, 1) == -1) {
- fprintf(stderr, "Unable to listen on socket: %s\n",
- strerror(errno));
- exit(1);
- }
-
- /*
- * Fork off a copy of ourselves, print out our child pid, then
- * exit.
- */
-
- switch (child = fork()) {
- case -1:
- fprintf(stderr, "Unable to fork child: %s\n", strerror(errno));
- exit(1);
- break;
- case 0:
- /*
- * Close stdin and stdout so $() in the shell will get an
- * EOF. For now leave stderr open.
- */
- fclose(stdin);
- fclose(stdout);
- break;
- default:
- printf("%ld\n", (long) child);
- exit(0);
- }
-
- /*
- * Now that our socket and files are set up, wait 30 seconds for
- * a connection. If there isn't one, then exit.
- */
-
- if (!(pid = fopen(PIDFILE, "w"))) {
- fprintf(stderr, "Cannot open " PIDFILE ": %s\n",
- strerror(errno));
- exit(1);
- }
-
- fprintf(pid, "%ld\n", (long) getpid());
- fclose(pid);
-
- signal(SIGTERM, handleterm);
- atexit(killpidfile);
-
- FD_ZERO(&readfd);
- FD_SET(l, &readfd);
-
- tv.tv_sec = 30;
- tv.tv_usec = 0;
-
- rc = select(l + 1, &readfd, NULL, NULL, &tv);
-
- if (rc < 0) {
- fprintf(stderr, "select() failed: %s\n", strerror(errno));
- exit(1);
- }
-
- /*
- * If we get a timeout, just silently exit
- */
-
- if (rc == 0) {
- exit(1);
- }
-
- /*
- * We got a connection; accept it. Right after that close our
- * listening socket so we won't get any more connections on it.
- */
-
- if ((s = accept(l, NULL, NULL)) == -1) {
- fprintf(stderr, "Unable to accept connection: %s\n",
- strerror(errno));
- exit(1);
- }
-
- close(l);
+ s = serve(PIDFILE, argv[1]);
/*
* Pretend to be a POP server
*/
- putpop(s, "+OK Not really a POP server, but we play one on TV");
+ putcrlf(s, "+OK Not really a POP server, but we play one on TV");
for (;;) {
char linebuf[LINESIZE];
if (strcasecmp(linebuf, "CAPA") == 0) {
putpopbulk(s, "+OK We have no capabilities, really\r\n"
- "FAKE-CAPABILITY\r\n.\r\n");
+ "FAKE-CAPABILITY\r\n");
+ if (xoauth != NULL) {
+ putcrlf(s, "SASL XOAUTH2");
+ }
+ putcrlf(s, ".");
} else if (strncasecmp(linebuf, "USER ", 5) == 0) {
if (strcmp(linebuf + 5, argv[2]) == 0) {
- putpop(s, "+OK Niiiice!");
+ putcrlf(s, "+OK Niiiice!");
user = 1;
} else {
- putpop(s, "-ERR Don't play me, bro!");
+ putcrlf(s, "-ERR Don't play me, bro!");
}
} else if (strncasecmp(linebuf, "PASS ", 5) == 0) {
CHECKUSER();
if (strcmp(linebuf + 5, argv[3]) == 0) {
- putpop(s, "+OK Aren't you a sight "
+ putcrlf(s, "+OK Aren't you a sight "
"for sore eyes!");
- pass = 1;
+ auth = 1;
} else {
- putpop(s, "-ERR C'mon!");
+ putcrlf(s, "-ERR C'mon!");
}
+ } else if (xoauth != NULL
+ && strncasecmp(linebuf, "AUTH XOAUTH2", 12) == 0) {
+ if (strstr(linebuf, xoauth) == NULL) {
+ putcrlf(s, "+ base64-json-err");
+ rc = getpop(s, linebuf, sizeof(linebuf));
+ if (rc != 0)
+ break; /* Error or EOF */
+ putcrlf(s, "-ERR [AUTH] Invalid credentials.");
+ continue;
+ }
+ putcrlf(s, "+OK Welcome.");
+ auth = 1;
} else if (strcasecmp(linebuf, "STAT") == 0) {
size_t total = 0;
- CHECKUSERPASS();
+ CHECKAUTH();
for (i = 0, j = 0; i < numfiles; i++) {
if (mfiles[i]) {
total += octets[i];
}
snprintf(linebuf, sizeof(linebuf),
"+OK %d %d", i, (int) total);
- putpop(s, linebuf);
+ putcrlf(s, linebuf);
} else if (strncasecmp(linebuf, "RETR ", 5) == 0) {
- CHECKUSERPASS();
+ CHECKAUTH();
rc = sscanf(linebuf + 5, "%d", &i);
if (rc != 1) {
- putpop(s, "-ERR Whaaaa...?");
+ putcrlf(s, "-ERR Whaaaa...?");
continue;
}
if (i < 1 || i > numfiles) {
- putpop(s, "-ERR That message number is "
+ putcrlf(s, "-ERR That message number is "
"out of range, jerkface!");
continue;
}
if (mfiles[i - 1] == NULL) {
- putpop(s, "-ERR Sorry, don't have it anymore");
+ putcrlf(s, "-ERR Sorry, don't have it anymore");
} else {
char *buf = readmessage(mfiles[i - 1]);
- putpop(s, "+OK Here you go ...");
+ putcrlf(s, "+OK Here you go ...");
putpopbulk(s, buf);
free(buf);
}
} else if (strncasecmp(linebuf, "DELE ", 5) == 0) {
- CHECKUSERPASS();
+ CHECKAUTH();
rc = sscanf(linebuf + 5, "%d", &i);
if (rc != 1) {
- putpop(s, "-ERR Whaaaa...?");
+ putcrlf(s, "-ERR Whaaaa...?");
continue;
}
if (i < 1 || i > numfiles) {
- putpop(s, "-ERR That message number is "
+ putcrlf(s, "-ERR That message number is "
"out of range, jerkface!");
continue;
}
if (mfiles[i - 1] == NULL) {
- putpop(s, "-ERR Um, didn't you tell me "
+ putcrlf(s, "-ERR Um, didn't you tell me "
"to delete it already?");
} else {
fclose(mfiles[i - 1]);
mfiles[i - 1] = NULL;
- putpop(s, "+OK Alright man, I got rid of it");
+ putcrlf(s, "+OK Alright man, I got rid of it");
}
} else if (strcasecmp(linebuf, "QUIT") == 0) {
- putpop(s, "+OK See ya, wouldn't want to be ya!");
+ putcrlf(s, "+OK See ya, wouldn't want to be ya!");
close(s);
break;
} else {
- putpop(s, "-ERR Um, what?");
+ putcrlf(s, "-ERR Um, what?");
}
}
exit(0);
}
-/*
- * Send one line to the POP client
- */
-
-static void
-putpop(int socket, char *data)
-{
- struct iovec iov[2];
-
- iov[0].iov_base = data;
- iov[0].iov_len = strlen(data);
- iov[1].iov_base = "\r\n";
- iov[1].iov_len = 2;
-
- if (writev(socket, iov, 2) < 0) {
- perror ("writev");
- }
-}
-
/*
* Put one big buffer to the POP server. Should have already had the line
* endings set up and dot-stuffed if necessary.
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);
-}
#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>
#define PIDFILE "/tmp/fakesmtp.pid"
#define LINESIZE 1024
-static void killpidfile(void);
-static void handleterm(int);
-static void putsmtp(int, char *);
+enum {
+ /* Processing top-level SMTP commands (e.g. EHLO, DATA). */
+ SMTP_TOP,
+
+ /* Processing payload of a DATA command. */
+ SMTP_DATA,
+
+ /* Looking for the blank line required by XOAUTH2 after 334 response. */
+ SMTP_XOAUTH_ERR
+};
+
+void putcrlf(int, char *);
+int serve(const char *, const char *);
+
static int getsmtp(int, char *);
int
main(int argc, char *argv[])
{
- struct addrinfo hints, *res;
- int rc, l, conn, on, datamode;
- FILE *f, *pid;
- pid_t child;
- fd_set readfd;
- struct stat st;
- struct timeval tv;
+ int rc, conn, smtp_state;
+ FILE *f;
+ const char *xoauth = getenv("XOAUTH");
if (argc != 3) {
fprintf(stderr, "Usage: %s output-filename port\n", argv[0]);
exit(1);
}
- /*
- * If there is a pid file already around, kill the previously running
- * fakesmtp process. Hopefully this will reduce the race conditions
- * that crop up when running the test suite.
- */
-
- if (stat(PIDFILE, &st) == 0) {
- long oldpid;
-
- if (!(pid = fopen(PIDFILE, "r"))) {
- fprintf(stderr, "Cannot open " PIDFILE
- " (%s), continuing ...\n", strerror(errno));
- } else {
- rc = fscanf(pid, "%ld", &oldpid);
- fclose(pid);
-
- if (rc != 1) {
- fprintf(stderr, "Unable to parse pid in "
- PIDFILE ", continuing ...\n");
- } else {
- kill((pid_t) oldpid, SIGTERM);
- }
- }
-
- unlink(PIDFILE);
- }
-
- memset(&hints, 0, sizeof(hints));
-
- hints.ai_family = PF_INET;
- hints.ai_socktype = SOCK_STREAM;
- hints.ai_protocol = IPPROTO_TCP;
- hints.ai_flags = AI_PASSIVE;
-
- rc = getaddrinfo("127.0.0.1", argv[2], &hints, &res);
-
- if (rc) {
- fprintf(stderr, "Unable to resolve localhost/%s: %s\n",
- argv[2], gai_strerror(rc));
- exit(1);
- }
-
- l = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
-
- if (l == -1) {
- fprintf(stderr, "Unable to create listening socket: %s\n",
- strerror(errno));
- exit(1);
- }
-
- on = 1;
-
- if (setsockopt(l, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1) {
- fprintf(stderr, "Unable to set SO_REUSEADDR: %s\n",
- strerror(errno));
- exit(1);
- }
-
- if (bind(l, res->ai_addr, res->ai_addrlen) == -1) {
- fprintf(stderr, "Unable to bind socket: %s\n", strerror(errno));
- exit(1);
- }
-
- if (listen(l, 1) == -1) {
- fprintf(stderr, "Unable to listen on socket: %s\n",
- strerror(errno));
- exit(1);
- }
-
- /*
- * Now we fork() and print out the process ID of our child
- * for scripts to use. Once we do that, then exit.
- */
-
- child = fork();
-
- switch (child) {
- case -1:
- fprintf(stderr, "Unable to fork child: %s\n", strerror(errno));
- exit(1);
- break;
- case 0:
- /*
- * Close stdin & stdout, otherwise people can
- * think we're still doing stuff. For now leave stderr
- * open.
- */
- fclose(stdin);
- fclose(stdout);
- break;
- default:
- printf("%ld\n", (long) child);
- exit(0);
- }
-
- /*
- * Now that our socket & files are set up, wait 30 seconds for
- * a connection. If there isn't one, then exit.
- */
-
- if (!(pid = fopen(PIDFILE, "w"))) {
- fprintf(stderr, "Cannot open " PIDFILE ": %s\n",
- strerror(errno));
- exit(1);
- }
-
- fprintf(pid, "%ld\n", (long) getpid());
- fclose(pid);
-
- signal(SIGTERM, handleterm);
- atexit(killpidfile);
-
- FD_ZERO(&readfd);
- FD_SET(l, &readfd);
- tv.tv_sec = 30;
- tv.tv_usec = 0;
-
- rc = select(l + 1, &readfd, NULL, NULL, &tv);
-
- if (rc < 0) {
- fprintf(stderr, "select() failed: %s\n", strerror(errno));
- exit(1);
- }
-
- /*
- * I think if we get a timeout, we should just exit quietly.
- */
-
- if (rc == 0) {
- exit(1);
- }
-
- /*
- * Alright, got a connection! Accept it.
- */
-
- if ((conn = accept(l, NULL, NULL)) == -1) {
- fprintf(stderr, "Unable to accept connection: %s\n",
- strerror(errno));
- exit(1);
- }
+ conn = serve(PIDFILE, argv[2]);
- close(l);
-
/*
* Pretend to be an SMTP server.
*/
- putsmtp(conn, "220 Not really an ESMTP server");
- datamode = 0;
+ putcrlf(conn, "220 Not really an ESMTP server");
+ smtp_state = SMTP_TOP;
for (;;) {
char line[LINESIZE];
fprintf(f, "%s\n", line);
- /*
- * If we're in DATA mode, then check to see if we've got
- * a "."; otherwise, continue
- */
-
- if (datamode) {
+ switch (smtp_state) {
+ case SMTP_DATA:
if (strcmp(line, ".") == 0) {
- datamode = 0;
- putsmtp(conn, "250 Thanks for the info!");
+ smtp_state = SMTP_TOP;
+ putcrlf(conn, "250 Thanks for the info!");
}
continue;
+ case SMTP_XOAUTH_ERR:
+ smtp_state = SMTP_TOP;
+ putcrlf(conn, "535 Not no way, not no how!");
+ continue;
}
/*
if (strcmp(line, "QUIT") == 0) {
fclose(f);
f = NULL;
- putsmtp(conn, "221 Later alligator!");
+ putcrlf(conn, "221 Later alligator!");
close(conn);
break;
- } else if (strcmp(line, "DATA") == 0) {
- putsmtp(conn, "354 Go ahead");
- datamode = 1;
- } else {
- putsmtp(conn, "250 I'll buy that for a dollar!");
}
+ if (strcmp(line, "DATA") == 0) {
+ putcrlf(conn, "354 Go ahead");
+ smtp_state = SMTP_DATA;
+ continue;
+ }
+ if (xoauth != NULL) {
+ /* XOAUTH2 support enabled; handle EHLO and AUTH. */
+ if (strncmp(line, "EHLO", 4) == 0) {
+ putcrlf(conn, "250-ready");
+ putcrlf(conn, "250 AUTH XOAUTH2");
+ continue;
+ }
+ if (strncmp(line, "AUTH", 4) == 0) {
+ if (strncmp(line, "AUTH XOAUTH2", 12) == 0
+ && strstr(line, xoauth) != NULL) {
+ putcrlf(conn, "235 OK come in");
+ continue;
+ }
+ putcrlf(conn, "334 base64-json-err");
+ smtp_state = SMTP_XOAUTH_ERR;
+ continue;
+ }
+ }
+ putcrlf(conn, "250 I'll buy that for a dollar!");
}
if (f)
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)
*/
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);
-}
--- /dev/null
+# 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
+User-Agent: nmh/${MH_VERSION} ${CURL_USER_AGENT}
+Host: 127.0.0.1:${http_port}
+Accept: */*
+Content-Length: $1
+Content-Type: application/x-www-form-urlencoded
+
+$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"
+}
--- /dev/null
+#!/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}
--- /dev/null
+#!/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}
--- /dev/null
+#!/bin/sh
+#
+# Test mhparam oauth
+#
+
+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"
+
+run_test "mhparam oauth" 'oauth'
--- /dev/null
+#!/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}
--- /dev/null
+#!/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}
--- /dev/null
+/*
+ * 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);
+ }
+}
--- /dev/null
+repo: 90642b15dce19adbb69f2b1792b263209079ce4b
+node: 19001fb4adb3914cd4f7584c828fa2e0ff7f4922
+branch: default
+latesttag: null
+latesttagdistance: 79
--- /dev/null
+Copyright (c) 2010 Serge A. Zaitsev
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
--- /dev/null
+# You can put your build options here
+-include config.mk
+
+all: libjsmn.a
+
+libjsmn.a: jsmn.o
+ $(AR) rc $@ $^
+
+%.o: %.c jsmn.h
+ $(CC) -c $(CFLAGS) $< -o $@
+
+test: jsmn_test
+ ./jsmn_test
+
+jsmn_test: jsmn_test.o
+ $(CC) $(LDFLAGS) -L. -ljsmn $< -o $@
+
+jsmn_test.o: jsmn_test.c libjsmn.a
+
+simple_example: example/simple.o libjsmn.a
+ $(CC) $(LDFLAGS) $^ -o $@
+
+jsondump: example/jsondump.o libjsmn.a
+ $(CC) $(LDFLAGS) $^ -o $@
+
+clean:
+ rm -f jsmn.o jsmn_test.o example/simple.o
+ rm -f jsmn_test
+ rm -f jsmn_test.exe
+ rm -f libjsmn.a
+ rm -f simple_example
+ rm -f jsondump
+
+.PHONY: all clean test
+
--- /dev/null
+
+JSMN
+====
+
+jsmn (pronounced like 'jasmine') is a minimalistic JSON parser in C. It can be
+easily integrated into resource-limited or embedded projects.
+
+You can find more information about JSON format at [json.org][1]
+
+Library sources are available at [bitbucket.org/zserge/jsmn][2]
+
+The web page with some information about jsmn can be found at
+[http://zserge.com/jsmn.html][3]
+
+Philosophy
+----------
+
+Most JSON parsers offer you a bunch of functions to load JSON data, parse it
+and extract any value by its name. jsmn proves that checking the correctness of
+every JSON packet or allocating temporary objects to store parsed JSON fields
+often is an overkill.
+
+JSON format itself is extremely simple, so why should we complicate it?
+
+jsmn is designed to be **robust** (it should work fine even with erroneous
+data), **fast** (it should parse data on the fly), **portable** (no superfluous
+dependencies or non-standard C extensions). An of course, **simplicity** is a
+key feature - simple code style, simple algorithm, simple integration into
+other projects.
+
+Features
+--------
+
+* compatible with C89
+* no dependencies (even libc!)
+* highly portable (tested on x86/amd64, ARM, AVR)
+* about 200 lines of code
+* extremely small code footprint
+* API contains only 2 functions
+* no dynamic memory allocation
+* incremental single-pass parsing
+* library code is covered with unit-tests
+
+Design
+------
+
+The rudimentary jsmn object is a **token**. Let's consider a JSON string:
+
+ '{ "name" : "Jack", "age" : 27 }'
+
+It holds the following tokens:
+
+* Object: `{ "name" : "Jack", "age" : 27}` (the whole object)
+* Strings: `"name"`, `"Jack"`, `"age"` (keys and some values)
+* Number: `27`
+
+In jsmn, tokens do not hold any data, but point to token boundaries in JSON
+string instead. In the example above jsmn will create tokens like: Object
+[0..31], String [3..7], String [12..16], String [20..23], Number [27..29].
+
+Every jsmn token has a type, which indicates the type of corresponding JSON
+token. jsmn supports the following token types:
+
+* Object - a container of key-value pairs, e.g.:
+ `{ "foo":"bar", "x":0.3 }`
+* Array - a sequence of values, e.g.:
+ `[ 1, 2, 3 ]`
+* String - a quoted sequence of chars, e.g.: `"foo"`
+* Primitive - a number, a boolean (`true`, `false`) or `null`
+
+Besides start/end positions, jsmn tokens for complex types (like arrays
+or objects) also contain a number of child items, so you can easily follow
+object hierarchy.
+
+This approach provides enough information for parsing any JSON data and makes
+it possible to use zero-copy techniques.
+
+Install
+-------
+
+To clone the repository you should have mercurial installed. Just run:
+
+ $ hg clone http://bitbucket.org/zserge/jsmn jsmn
+
+Repository layout is simple: jsmn.c and jsmn.h are library files, tests are in
+the jsmn\_test.c, you will also find README, LICENSE and Makefile files inside.
+
+To build the library, run `make`. It is also recommended to run `make test`.
+Let me know, if some tests fail.
+
+If build was successful, you should get a `libjsmn.a` library.
+The header file you should include is called `"jsmn.h"`.
+
+API
+---
+
+Token types are described by `jsmntype_t`:
+
+ typedef enum {
+ JSMN_PRIMITIVE = 0,
+ JSMN_OBJECT = 1,
+ JSMN_ARRAY = 2,
+ JSMN_STRING = 3
+ } jsmntype_t;
+
+**Note:** Unlike JSON data types, primitive tokens are not divided into
+numbers, booleans and null, because one can easily tell the type using the
+first character:
+
+* <code>'t', 'f'</code> - boolean
+* <code>'n'</code> - null
+* <code>'-', '0'..'9'</code> - number
+
+Token is an object of `jsmntok_t` type:
+
+ typedef struct {
+ jsmntype_t type; // Token type
+ int start; // Token start position
+ int end; // Token end position
+ int size; // Number of child (nested) tokens
+ } jsmntok_t;
+
+**Note:** string tokens point to the first character after
+the opening quote and the previous symbol before final quote. This was made
+to simplify string extraction from JSON data.
+
+All job is done by `jsmn_parser` object. You can initialize a new parser using:
+
+ struct jsmn_parser parser;
+ jsmntok_t tokens[10];
+
+ // js - pointer to JSON string
+ // tokens - an array of tokens available
+ // 10 - number of tokens available
+ jsmn_init_parser(&parser, js, tokens, 10);
+
+This will create a parser, that can parse up to 10 JSON tokens from `js` string.
+
+Later, you can use `jsmn_parse(&parser)` function to process JSON string with the parser.
+
+A non-negative value is the number of tokens actually used by the parser.
+Passing NULL instead of the tokens array would not store parsing results, but
+instead the function will return the value of tokens needed to parse the given
+string. This can be useful if you don't know yet how many tokens to allocate.
+
+If something goes wrong, you will get an error. Error will be one of these:
+
+* `JSMN_ERROR_INVAL` - bad token, JSON string is corrupted
+* `JSMN_ERROR_NOMEM` - not enough tokens, JSON string is too large
+* `JSMN_ERROR_PART` - JSON string is too short, expecting more JSON data
+
+If you get `JSON_ERROR_NOMEM`, you can re-allocate more tokens and call
+`jsmn_parse` once more. If you read json data from the stream, you can
+periodically call `jsmn_parse` and check if return value is `JSON_ERROR_PART`.
+You will get this error until you reach the end of JSON data.
+
+Other info
+----------
+
+This software is distributed under [MIT license](http://www.opensource.org/licenses/mit-license.php),
+ so feel free to integrate it in your commercial products.
+
+[1]: http://www.json.org/
+[2]: https://bitbucket.org/zserge/jsmn/wiki/Home
+[3]: http://zserge.com/jsmn.html
--- /dev/null
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <errno.h>
+#include "../jsmn.h"
+
+/*
+ * An example of reading JSON from stdin and printing its content to stdout.
+ * The output looks like YAML, but I'm not sure if it's really compatible.
+ */
+
+static int dump(const char *js, jsmntok_t *t, size_t count, int indent) {
+ int i, j, k;
+ if (count == 0) {
+ return 0;
+ }
+ if (t->type == JSMN_PRIMITIVE) {
+ printf("%.*s", t->end - t->start, js+t->start);
+ return 1;
+ } else if (t->type == JSMN_STRING) {
+ printf("'%.*s'", t->end - t->start, js+t->start);
+ return 1;
+ } else if (t->type == JSMN_OBJECT) {
+ printf("\n");
+ j = 0;
+ for (i = 0; i < t->size; i++) {
+ for (k = 0; k < indent; k++) printf(" ");
+ j += dump(js, t+1+j, count-j, indent+1);
+ printf(": ");
+ j += dump(js, t+1+j, count-j, indent+1);
+ printf("\n");
+ }
+ return j+1;
+ } else if (t->type == JSMN_ARRAY) {
+ j = 0;
+ printf("\n");
+ for (i = 0; i < t->size; i++) {
+ for (k = 0; k < indent-1; k++) printf(" ");
+ printf(" - ");
+ j += dump(js, t+1+j, count-j, indent+1);
+ printf("\n");
+ }
+ return j+1;
+ }
+ return 0;
+}
+
+int main() {
+ int r;
+ int eof_expected = 0;
+ char *js = NULL;
+ size_t jslen = 0;
+ char buf[BUFSIZ];
+
+ jsmn_parser p;
+ jsmntok_t *tok;
+ size_t tokcount = 2;
+
+ /* Prepare parser */
+ jsmn_init(&p);
+
+ /* Allocate some tokens as a start */
+ tok = malloc(sizeof(*tok) * tokcount);
+ if (tok == NULL) {
+ fprintf(stderr, "malloc(): errno=%d\n", errno);
+ return 3;
+ }
+
+ for (;;) {
+ /* Read another chunk */
+ r = fread(buf, 1, sizeof(buf), stdin);
+ if (r < 0) {
+ fprintf(stderr, "fread(): %d, errno=%d\n", r, errno);
+ return 1;
+ }
+ if (r == 0) {
+ if (eof_expected != 0) {
+ return 0;
+ } else {
+ fprintf(stderr, "fread(): unexpected EOF\n");
+ return 2;
+ }
+ }
+
+ js = realloc(js, jslen + r + 1);
+ if (js == NULL) {
+ fprintf(stderr, "realloc(): errno=%d\n", errno);
+ return 3;
+ }
+ strncpy(js + jslen, buf, r);
+ jslen = jslen + r;
+
+again:
+ r = jsmn_parse(&p, js, jslen, tok, tokcount);
+ if (r < 0) {
+ if (r == JSMN_ERROR_NOMEM) {
+ tokcount = tokcount * 2;
+ tok = realloc(tok, sizeof(*tok) * tokcount);
+ if (tok == NULL) {
+ fprintf(stderr, "realloc(): errno=%d\n", errno);
+ return 3;
+ }
+ goto again;
+ }
+ } else {
+ dump(js, tok, p.toknext, 0);
+ eof_expected = 1;
+ }
+ }
+
+ return 0;
+}
--- /dev/null
+#include <stdio.h>
+#include <string.h>
+#include "../jsmn.h"
+
+/*
+ * A small example of jsmn parsing when JSON structure is known and number of
+ * tokens is predictable.
+ */
+
+const char *JSON_STRING =
+ "{\"user\": \"johndoe\", \"admin\": false, \"uid\": 1000,\n "
+ "\"groups\": [\"users\", \"wheel\", \"audio\", \"video\"]}";
+
+static int jsoneq(const char *json, jsmntok_t *tok, const char *s) {
+ if (tok->type == JSMN_STRING && (int) strlen(s) == tok->end - tok->start &&
+ strncmp(json + tok->start, s, tok->end - tok->start) == 0) {
+ return 0;
+ }
+ return -1;
+}
+
+int main() {
+ int i;
+ int r;
+ jsmn_parser p;
+ jsmntok_t t[128]; /* We expect no more than 128 tokens */
+
+ jsmn_init(&p);
+ r = jsmn_parse(&p, JSON_STRING, strlen(JSON_STRING), t, sizeof(t)/sizeof(t[0]));
+ if (r < 0) {
+ printf("Failed to parse JSON: %d\n", r);
+ return 1;
+ }
+
+ /* Assume the top-level element is an object */
+ if (r < 1 || t[0].type != JSMN_OBJECT) {
+ printf("Object expected\n");
+ return 1;
+ }
+
+ /* Loop over all keys of the root object */
+ for (i = 1; i < r; i++) {
+ if (jsoneq(JSON_STRING, &t[i], "user") == 0) {
+ /* We may use strndup() to fetch string value */
+ printf("- User: %.*s\n", t[i+1].end-t[i+1].start,
+ JSON_STRING + t[i+1].start);
+ i++;
+ } else if (jsoneq(JSON_STRING, &t[i], "admin") == 0) {
+ /* We may additionally check if the value is either "true" or "false" */
+ printf("- Admin: %.*s\n", t[i+1].end-t[i+1].start,
+ JSON_STRING + t[i+1].start);
+ i++;
+ } else if (jsoneq(JSON_STRING, &t[i], "uid") == 0) {
+ /* We may want to do strtol() here to get numeric value */
+ printf("- UID: %.*s\n", t[i+1].end-t[i+1].start,
+ JSON_STRING + t[i+1].start);
+ i++;
+ } else if (jsoneq(JSON_STRING, &t[i], "groups") == 0) {
+ int j;
+ printf("- Groups:\n");
+ if (t[i+1].type != JSMN_ARRAY) {
+ continue; /* We expect groups to be an array of strings */
+ }
+ for (j = 0; j < t[i+1].size; j++) {
+ jsmntok_t *g = &t[i+j+2];
+ printf(" * %.*s\n", g->end - g->start, JSON_STRING + g->start);
+ }
+ i += t[i+1].size + 1;
+ } else {
+ printf("Unexpected key: %.*s\n", t[i].end-t[i].start,
+ JSON_STRING + t[i].start);
+ }
+ }
+ return 0;
+}
--- /dev/null
+#include <stdlib.h>
+
+#include "jsmn.h"
+
+/**
+ * Allocates a fresh unused token from the token pull.
+ */
+static jsmntok_t *jsmn_alloc_token(jsmn_parser *parser,
+ jsmntok_t *tokens, size_t num_tokens) {
+ jsmntok_t *tok;
+ if (parser->toknext >= num_tokens) {
+ return NULL;
+ }
+ tok = &tokens[parser->toknext++];
+ tok->start = tok->end = -1;
+ tok->size = 0;
+#ifdef JSMN_PARENT_LINKS
+ tok->parent = -1;
+#endif
+ return tok;
+}
+
+/**
+ * Fills token type and boundaries.
+ */
+static void jsmn_fill_token(jsmntok_t *token, jsmntype_t type,
+ int start, int end) {
+ token->type = type;
+ token->start = start;
+ token->end = end;
+ token->size = 0;
+}
+
+/**
+ * Fills next available token with JSON primitive.
+ */
+static jsmnerr_t jsmn_parse_primitive(jsmn_parser *parser, const char *js,
+ size_t len, jsmntok_t *tokens, size_t num_tokens) {
+ jsmntok_t *token;
+ int start;
+
+ start = parser->pos;
+
+ for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) {
+ switch (js[parser->pos]) {
+#ifndef JSMN_STRICT
+ /* In strict mode primitive must be followed by "," or "}" or "]" */
+ case ':':
+#endif
+ case '\t' : case '\r' : case '\n' : case ' ' :
+ case ',' : case ']' : case '}' :
+ goto found;
+ }
+ if (js[parser->pos] < 32 || js[parser->pos] >= 127) {
+ parser->pos = start;
+ return JSMN_ERROR_INVAL;
+ }
+ }
+#ifdef JSMN_STRICT
+ /* In strict mode primitive must be followed by a comma/object/array */
+ parser->pos = start;
+ return JSMN_ERROR_PART;
+#endif
+
+found:
+ if (tokens == NULL) {
+ parser->pos--;
+ return 0;
+ }
+ token = jsmn_alloc_token(parser, tokens, num_tokens);
+ if (token == NULL) {
+ parser->pos = start;
+ return JSMN_ERROR_NOMEM;
+ }
+ jsmn_fill_token(token, JSMN_PRIMITIVE, start, parser->pos);
+#ifdef JSMN_PARENT_LINKS
+ token->parent = parser->toksuper;
+#endif
+ parser->pos--;
+ return 0;
+}
+
+/**
+ * Filsl next token with JSON string.
+ */
+static jsmnerr_t jsmn_parse_string(jsmn_parser *parser, const char *js,
+ size_t len, jsmntok_t *tokens, size_t num_tokens) {
+ jsmntok_t *token;
+
+ int start = parser->pos;
+
+ parser->pos++;
+
+ /* Skip starting quote */
+ for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) {
+ char c = js[parser->pos];
+
+ /* Quote: end of string */
+ if (c == '\"') {
+ if (tokens == NULL) {
+ return 0;
+ }
+ token = jsmn_alloc_token(parser, tokens, num_tokens);
+ if (token == NULL) {
+ parser->pos = start;
+ return JSMN_ERROR_NOMEM;
+ }
+ jsmn_fill_token(token, JSMN_STRING, start+1, parser->pos);
+#ifdef JSMN_PARENT_LINKS
+ token->parent = parser->toksuper;
+#endif
+ return 0;
+ }
+
+ /* Backslash: Quoted symbol expected */
+ if (c == '\\' && parser->pos + 1 < len) {
+ int i;
+ parser->pos++;
+ switch (js[parser->pos]) {
+ /* Allowed escaped symbols */
+ case '\"': case '/' : case '\\' : case 'b' :
+ case 'f' : case 'r' : case 'n' : case 't' :
+ break;
+ /* Allows escaped symbol \uXXXX */
+ case 'u':
+ parser->pos++;
+ for(i = 0; i < 4 && parser->pos < len && js[parser->pos] != '\0'; i++) {
+ /* If it isn't a hex character we have an error */
+ if(!((js[parser->pos] >= 48 && js[parser->pos] <= 57) || /* 0-9 */
+ (js[parser->pos] >= 65 && js[parser->pos] <= 70) || /* A-F */
+ (js[parser->pos] >= 97 && js[parser->pos] <= 102))) { /* a-f */
+ parser->pos = start;
+ return JSMN_ERROR_INVAL;
+ }
+ parser->pos++;
+ }
+ parser->pos--;
+ break;
+ /* Unexpected symbol */
+ default:
+ parser->pos = start;
+ return JSMN_ERROR_INVAL;
+ }
+ }
+ }
+ parser->pos = start;
+ return JSMN_ERROR_PART;
+}
+
+/**
+ * Parse JSON string and fill tokens.
+ */
+jsmnerr_t jsmn_parse(jsmn_parser *parser, const char *js, size_t len,
+ jsmntok_t *tokens, unsigned int num_tokens) {
+ jsmnerr_t r;
+ int i;
+ jsmntok_t *token;
+ int count = 0;
+
+ for (; parser->pos < len && js[parser->pos] != '\0'; parser->pos++) {
+ char c;
+ jsmntype_t type;
+
+ c = js[parser->pos];
+ switch (c) {
+ case '{': case '[':
+ count++;
+ if (tokens == NULL) {
+ break;
+ }
+ token = jsmn_alloc_token(parser, tokens, num_tokens);
+ if (token == NULL)
+ return JSMN_ERROR_NOMEM;
+ if (parser->toksuper != -1) {
+ tokens[parser->toksuper].size++;
+#ifdef JSMN_PARENT_LINKS
+ token->parent = parser->toksuper;
+#endif
+ }
+ token->type = (c == '{' ? JSMN_OBJECT : JSMN_ARRAY);
+ token->start = parser->pos;
+ parser->toksuper = parser->toknext - 1;
+ break;
+ case '}': case ']':
+ if (tokens == NULL)
+ break;
+ type = (c == '}' ? JSMN_OBJECT : JSMN_ARRAY);
+#ifdef JSMN_PARENT_LINKS
+ if (parser->toknext < 1) {
+ return JSMN_ERROR_INVAL;
+ }
+ token = &tokens[parser->toknext - 1];
+ for (;;) {
+ if (token->start != -1 && token->end == -1) {
+ if (token->type != type) {
+ return JSMN_ERROR_INVAL;
+ }
+ token->end = parser->pos + 1;
+ parser->toksuper = token->parent;
+ break;
+ }
+ if (token->parent == -1) {
+ break;
+ }
+ token = &tokens[token->parent];
+ }
+#else
+ for (i = parser->toknext - 1; i >= 0; i--) {
+ token = &tokens[i];
+ if (token->start != -1 && token->end == -1) {
+ if (token->type != type) {
+ return JSMN_ERROR_INVAL;
+ }
+ parser->toksuper = -1;
+ token->end = parser->pos + 1;
+ break;
+ }
+ }
+ /* Error if unmatched closing bracket */
+ if (i == -1) return JSMN_ERROR_INVAL;
+ for (; i >= 0; i--) {
+ token = &tokens[i];
+ if (token->start != -1 && token->end == -1) {
+ parser->toksuper = i;
+ break;
+ }
+ }
+#endif
+ break;
+ case '\"':
+ r = jsmn_parse_string(parser, js, len, tokens, num_tokens);
+ if (r < 0) return r;
+ count++;
+ if (parser->toksuper != -1 && tokens != NULL)
+ tokens[parser->toksuper].size++;
+ break;
+ case '\t' : case '\r' : case '\n' : case ' ':
+ break;
+ case ':':
+ parser->toksuper = parser->toknext - 1;
+ break;
+ case ',':
+ if (tokens != NULL &&
+ tokens[parser->toksuper].type != JSMN_ARRAY &&
+ tokens[parser->toksuper].type != JSMN_OBJECT) {
+#ifdef JSMN_PARENT_LINKS
+ parser->toksuper = tokens[parser->toksuper].parent;
+#else
+ for (i = parser->toknext - 1; i >= 0; i--) {
+ if (tokens[i].type == JSMN_ARRAY || tokens[i].type == JSMN_OBJECT) {
+ if (tokens[i].start != -1 && tokens[i].end == -1) {
+ parser->toksuper = i;
+ break;
+ }
+ }
+ }
+#endif
+ }
+ break;
+#ifdef JSMN_STRICT
+ /* In strict mode primitives are: numbers and booleans */
+ case '-': case '0': case '1' : case '2': case '3' : case '4':
+ case '5': case '6': case '7' : case '8': case '9':
+ case 't': case 'f': case 'n' :
+ /* And they must not be keys of the object */
+ if (tokens != NULL) {
+ jsmntok_t *t = &tokens[parser->toksuper];
+ if (t->type == JSMN_OBJECT ||
+ (t->type == JSMN_STRING && t->size != 0)) {
+ return JSMN_ERROR_INVAL;
+ }
+ }
+#else
+ /* In non-strict mode every unquoted value is a primitive */
+ default:
+#endif
+ r = jsmn_parse_primitive(parser, js, len, tokens, num_tokens);
+ if (r < 0) return r;
+ count++;
+ if (parser->toksuper != -1 && tokens != NULL)
+ tokens[parser->toksuper].size++;
+ break;
+
+#ifdef JSMN_STRICT
+ /* Unexpected char in strict mode */
+ default:
+ return JSMN_ERROR_INVAL;
+#endif
+ }
+ }
+
+ for (i = parser->toknext - 1; i >= 0; i--) {
+ /* Unmatched opened object or array */
+ if (tokens[i].start != -1 && tokens[i].end == -1) {
+ return JSMN_ERROR_PART;
+ }
+ }
+
+ return count;
+}
+
+/**
+ * Creates a new parser based over a given buffer with an array of tokens
+ * available.
+ */
+void jsmn_init(jsmn_parser *parser) {
+ parser->pos = 0;
+ parser->toknext = 0;
+ parser->toksuper = -1;
+}
+
--- /dev/null
+#ifndef __JSMN_H_
+#define __JSMN_H_
+
+#include <stddef.h>
+
+#ifdef __cplusplus
+extern "C" {
+#endif
+
+/**
+ * JSON type identifier. Basic types are:
+ * o Object
+ * o Array
+ * o String
+ * o Other primitive: number, boolean (true/false) or null
+ */
+typedef enum {
+ JSMN_PRIMITIVE = 0,
+ JSMN_OBJECT = 1,
+ JSMN_ARRAY = 2,
+ JSMN_STRING = 3
+} jsmntype_t;
+
+typedef enum {
+ /* Not enough tokens were provided */
+ JSMN_ERROR_NOMEM = -1,
+ /* Invalid character inside JSON string */
+ JSMN_ERROR_INVAL = -2,
+ /* The string is not a full JSON packet, more bytes expected */
+ JSMN_ERROR_PART = -3
+} jsmnerr_t;
+
+/**
+ * JSON token description.
+ * @param type type (object, array, string etc.)
+ * @param start start position in JSON data string
+ * @param end end position in JSON data string
+ */
+typedef struct {
+ jsmntype_t type;
+ int start;
+ int end;
+ int size;
+#ifdef JSMN_PARENT_LINKS
+ int parent;
+#endif
+} jsmntok_t;
+
+/**
+ * JSON parser. Contains an array of token blocks available. Also stores
+ * the string being parsed now and current position in that string
+ */
+typedef struct {
+ unsigned int pos; /* offset in the JSON string */
+ unsigned int toknext; /* next token to allocate */
+ int toksuper; /* superior token node, e.g parent object or array */
+} jsmn_parser;
+
+/**
+ * Create JSON parser over an array of tokens
+ */
+void jsmn_init(jsmn_parser *parser);
+
+/**
+ * Run JSON parser. It parses a JSON data string into and array of tokens, each describing
+ * a single JSON object.
+ */
+jsmnerr_t jsmn_parse(jsmn_parser *parser, const char *js, size_t len,
+ jsmntok_t *tokens, unsigned int num_tokens);
+
+#ifdef __cplusplus
+}
+#endif
+
+#endif /* __JSMN_H_ */
--- /dev/null
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+static int test_passed = 0;
+static int test_failed = 0;
+
+/* Terminate current test with error */
+#define fail() return __LINE__
+
+/* Successfull end of the test case */
+#define done() return 0
+
+/* Check single condition */
+#define check(cond) do { if (!(cond)) fail(); } while (0)
+
+/* Test runner */
+static void test(int (*func)(void), const char *name) {
+ int r = func();
+ if (r == 0) {
+ test_passed++;
+ } else {
+ test_failed++;
+ printf("FAILED: %s (at line %d)\n", name, r);
+ }
+}
+
+#define TOKEN_EQ(t, tok_start, tok_end, tok_type) \
+ ((t).start == tok_start \
+ && (t).end == tok_end \
+ && (t).type == (tok_type))
+
+#define TOKEN_STRING(js, t, s) \
+ (strncmp(js+(t).start, s, (t).end - (t).start) == 0 \
+ && strlen(s) == (t).end - (t).start)
+
+#define TOKEN_PRINT(t) \
+ printf("start: %d, end: %d, type: %d, size: %d\n", \
+ (t).start, (t).end, (t).type, (t).size)
+
+#define JSMN_STRICT
+#include "jsmn.c"
+
+int test_empty() {
+ const char *js;
+ int r;
+ jsmn_parser p;
+ jsmntok_t t[10];
+
+ js = "{}";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), t, 10);
+ check(r >= 0);
+ check(t[0].type == JSMN_OBJECT);
+ check(t[0].start == 0 && t[0].end == 2);
+
+ js = "[]";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), t, 10);
+ check(r >= 0);
+ check(t[0].type == JSMN_ARRAY);
+ check(t[0].start == 0 && t[0].end == 2);
+
+ js = "{\"a\":[]}";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), t, 10);
+ check(r >= 0);
+ check(t[0].type == JSMN_OBJECT && t[0].start == 0 && t[0].end == 8);
+ check(t[1].type == JSMN_STRING && t[1].start == 2 && t[1].end == 3);
+ check(t[2].type == JSMN_ARRAY && t[2].start == 5 && t[2].end == 7);
+
+ js = "[{},{}]";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), t, 10);
+ check(r >= 0);
+ check(t[0].type == JSMN_ARRAY && t[0].start == 0 && t[0].end == 7);
+ check(t[1].type == JSMN_OBJECT && t[1].start == 1 && t[1].end == 3);
+ check(t[2].type == JSMN_OBJECT && t[2].start == 4 && t[2].end == 6);
+ return 0;
+}
+
+int test_simple() {
+ const char *js;
+ int r;
+ jsmn_parser p;
+ jsmntok_t tokens[10];
+
+ js = "{\"a\": 0}";
+
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), tokens, 10);
+ check(r >= 0);
+ check(TOKEN_EQ(tokens[0], 0, 8, JSMN_OBJECT));
+ check(TOKEN_EQ(tokens[1], 2, 3, JSMN_STRING));
+ check(TOKEN_EQ(tokens[2], 6, 7, JSMN_PRIMITIVE));
+
+ check(TOKEN_STRING(js, tokens[0], js));
+ check(TOKEN_STRING(js, tokens[1], "a"));
+ check(TOKEN_STRING(js, tokens[2], "0"));
+
+ jsmn_init(&p);
+ js = "[\"a\":{},\"b\":{}]";
+ r = jsmn_parse(&p, js, strlen(js), tokens, 10);
+ check(r >= 0);
+
+ jsmn_init(&p);
+ js = "{\n \"Day\": 26,\n \"Month\": 9,\n \"Year\": 12\n }";
+ r = jsmn_parse(&p, js, strlen(js), tokens, 10);
+ check(r >= 0);
+
+ return 0;
+}
+
+int test_primitive() {
+#ifndef JSMN_STRICT
+ int r;
+ jsmn_parser p;
+ jsmntok_t tok[10];
+ const char *js;
+ js = "\"boolVar\" : true";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), tok, 10);
+ check(r >= 0 && tok[0].type == JSMN_STRING
+ && tok[1].type == JSMN_PRIMITIVE);
+ check(TOKEN_STRING(js, tok[0], "boolVar"));
+ check(TOKEN_STRING(js, tok[1], "true"));
+
+ js = "\"boolVar\" : false";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), tok, 10);
+ check(r >= 0 && tok[0].type == JSMN_STRING
+ && tok[1].type == JSMN_PRIMITIVE);
+ check(TOKEN_STRING(js, tok[0], "boolVar"));
+ check(TOKEN_STRING(js, tok[1], "false"));
+
+ js = "\"intVar\" : 12345";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), tok, 10);
+ check(r >= 0 && tok[0].type == JSMN_STRING
+ && tok[1].type == JSMN_PRIMITIVE);
+ check(TOKEN_STRING(js, tok[0], "intVar"));
+ check(TOKEN_STRING(js, tok[1], "12345"));
+
+ js = "\"floatVar\" : 12.345";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), tok, 10);
+ check(r >= 0 && tok[0].type == JSMN_STRING
+ && tok[1].type == JSMN_PRIMITIVE);
+ check(TOKEN_STRING(js, tok[0], "floatVar"));
+ check(TOKEN_STRING(js, tok[1], "12.345"));
+
+ js = "\"nullVar\" : null";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), tok, 10);
+ check(r >= 0 && tok[0].type == JSMN_STRING
+ && tok[1].type == JSMN_PRIMITIVE);
+ check(TOKEN_STRING(js, tok[0], "nullVar"));
+ check(TOKEN_STRING(js, tok[1], "null"));
+#endif
+ return 0;
+}
+
+int test_string() {
+ int r;
+ jsmn_parser p;
+ jsmntok_t tok[10];
+ const char *js;
+
+ js = "\"strVar\" : \"hello world\"";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), tok, 10);
+ check(r >= 0 && tok[0].type == JSMN_STRING
+ && tok[1].type == JSMN_STRING);
+ check(TOKEN_STRING(js, tok[0], "strVar"));
+ check(TOKEN_STRING(js, tok[1], "hello world"));
+
+ js = "\"strVar\" : \"escapes: \\/\\r\\n\\t\\b\\f\\\"\\\\\"";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), tok, 10);
+ check(r >= 0 && tok[0].type == JSMN_STRING
+ && tok[1].type == JSMN_STRING);
+ check(TOKEN_STRING(js, tok[0], "strVar"));
+ check(TOKEN_STRING(js, tok[1], "escapes: \\/\\r\\n\\t\\b\\f\\\"\\\\"));
+
+ js = "\"strVar\" : \"\"";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), tok, 10);
+ check(r >= 0 && tok[0].type == JSMN_STRING
+ && tok[1].type == JSMN_STRING);
+ check(TOKEN_STRING(js, tok[0], "strVar"));
+ check(TOKEN_STRING(js, tok[1], ""));
+
+ return 0;
+}
+
+int test_partial_string() {
+ int r;
+ jsmn_parser p;
+ jsmntok_t tok[10];
+ const char *js;
+
+ jsmn_init(&p);
+ js = "\"x\": \"va";
+ r = jsmn_parse(&p, js, strlen(js), tok, 10);
+ check(r == JSMN_ERROR_PART && tok[0].type == JSMN_STRING);
+ check(TOKEN_STRING(js, tok[0], "x"));
+ check(p.toknext == 1);
+
+ jsmn_init(&p);
+ char js_slash[9] = "\"x\": \"va\\";
+ r = jsmn_parse(&p, js_slash, sizeof(js_slash), tok, 10);
+ check(r == JSMN_ERROR_PART);
+
+ jsmn_init(&p);
+ char js_unicode[10] = "\"x\": \"va\\u";
+ r = jsmn_parse(&p, js_unicode, sizeof(js_unicode), tok, 10);
+ check(r == JSMN_ERROR_PART);
+
+ js = "\"x\": \"valu";
+ r = jsmn_parse(&p, js, strlen(js), tok, 10);
+ check(r == JSMN_ERROR_PART && tok[0].type == JSMN_STRING);
+ check(TOKEN_STRING(js, tok[0], "x"));
+ check(p.toknext == 1);
+
+ js = "\"x\": \"value\"";
+ r = jsmn_parse(&p, js, strlen(js), tok, 10);
+ check(r >= 0 && tok[0].type == JSMN_STRING
+ && tok[1].type == JSMN_STRING);
+ check(TOKEN_STRING(js, tok[0], "x"));
+ check(TOKEN_STRING(js, tok[1], "value"));
+
+ js = "\"x\": \"value\", \"y\": \"value y\"";
+ r = jsmn_parse(&p, js, strlen(js), tok, 10);
+ check(r >= 0 && tok[0].type == JSMN_STRING
+ && tok[1].type == JSMN_STRING && tok[2].type == JSMN_STRING
+ && tok[3].type == JSMN_STRING);
+ check(TOKEN_STRING(js, tok[0], "x"));
+ check(TOKEN_STRING(js, tok[1], "value"));
+ check(TOKEN_STRING(js, tok[2], "y"));
+ check(TOKEN_STRING(js, tok[3], "value y"));
+
+ return 0;
+}
+
+int test_unquoted_keys() {
+#ifndef JSMN_STRICT
+ int r;
+ jsmn_parser p;
+ jsmntok_t tok[10];
+ const char *js;
+
+ jsmn_init(&p);
+ js = "key1: \"value\"\nkey2 : 123";
+
+ r = jsmn_parse(&p, js, strlen(js), tok, 10);
+ check(r >= 0 && tok[0].type == JSMN_PRIMITIVE
+ && tok[1].type == JSMN_STRING && tok[2].type == JSMN_PRIMITIVE
+ && tok[3].type == JSMN_PRIMITIVE);
+ check(TOKEN_STRING(js, tok[0], "key1"));
+ check(TOKEN_STRING(js, tok[1], "value"));
+ check(TOKEN_STRING(js, tok[2], "key2"));
+ check(TOKEN_STRING(js, tok[3], "123"));
+#endif
+ return 0;
+}
+
+int test_partial_array() {
+ int r;
+ jsmn_parser p;
+ jsmntok_t tok[10];
+ const char *js;
+
+ jsmn_init(&p);
+ js = " [ 1, true, ";
+ r = jsmn_parse(&p, js, strlen(js), tok, 10);
+ check(r == JSMN_ERROR_PART && tok[0].type == JSMN_ARRAY
+ && tok[1].type == JSMN_PRIMITIVE && tok[2].type == JSMN_PRIMITIVE);
+
+ js = " [ 1, true, [123, \"hello";
+ r = jsmn_parse(&p, js, strlen(js), tok, 10);
+ check(r == JSMN_ERROR_PART && tok[0].type == JSMN_ARRAY
+ && tok[1].type == JSMN_PRIMITIVE && tok[2].type == JSMN_PRIMITIVE
+ && tok[3].type == JSMN_ARRAY && tok[4].type == JSMN_PRIMITIVE);
+
+ js = " [ 1, true, [123, \"hello\"]";
+ r = jsmn_parse(&p, js, strlen(js), tok, 10);
+ check(r == JSMN_ERROR_PART && tok[0].type == JSMN_ARRAY
+ && tok[1].type == JSMN_PRIMITIVE && tok[2].type == JSMN_PRIMITIVE
+ && tok[3].type == JSMN_ARRAY && tok[4].type == JSMN_PRIMITIVE
+ && tok[5].type == JSMN_STRING);
+ /* check child nodes of the 2nd array */
+ check(tok[3].size == 2);
+
+ js = " [ 1, true, [123, \"hello\"]]";
+ r = jsmn_parse(&p, js, strlen(js), tok, 10);
+ check(r >= 0 && tok[0].type == JSMN_ARRAY
+ && tok[1].type == JSMN_PRIMITIVE && tok[2].type == JSMN_PRIMITIVE
+ && tok[3].type == JSMN_ARRAY && tok[4].type == JSMN_PRIMITIVE
+ && tok[5].type == JSMN_STRING);
+ check(tok[3].size == 2);
+ check(tok[0].size == 3);
+ return 0;
+}
+
+int test_array_nomem() {
+ int i;
+ int r;
+ jsmn_parser p;
+ jsmntok_t toksmall[10], toklarge[10];
+ const char *js;
+
+ js = " [ 1, true, [123, \"hello\"]]";
+
+ for (i = 0; i < 6; i++) {
+ jsmn_init(&p);
+ memset(toksmall, 0, sizeof(toksmall));
+ memset(toklarge, 0, sizeof(toklarge));
+ r = jsmn_parse(&p, js, strlen(js), toksmall, i);
+ check(r == JSMN_ERROR_NOMEM);
+
+ memcpy(toklarge, toksmall, sizeof(toksmall));
+
+ r = jsmn_parse(&p, js, strlen(js), toklarge, 10);
+ check(r >= 0);
+
+ check(toklarge[0].type == JSMN_ARRAY && toklarge[0].size == 3);
+ check(toklarge[3].type == JSMN_ARRAY && toklarge[3].size == 2);
+ }
+ return 0;
+}
+
+int test_objects_arrays() {
+ int r;
+ jsmn_parser p;
+ jsmntok_t tokens[10];
+ const char *js;
+
+ js = "[10}";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), tokens, 10);
+ check(r == JSMN_ERROR_INVAL);
+
+ js = "[10]";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), tokens, 10);
+ check(r >= 0);
+
+ js = "{\"a\": 1]";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), tokens, 10);
+ check(r == JSMN_ERROR_INVAL);
+
+ js = "{\"a\": 1}";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), tokens, 10);
+ check(r >= 0);
+
+ return 0;
+}
+
+int test_issue_22() {
+ int r;
+ jsmn_parser p;
+ jsmntok_t tokens[128];
+ const char *js;
+
+ js = "{ \"height\":10, \"layers\":[ { \"data\":[6,6], \"height\":10, "
+ "\"name\":\"Calque de Tile 1\", \"opacity\":1, \"type\":\"tilelayer\", "
+ "\"visible\":true, \"width\":10, \"x\":0, \"y\":0 }], "
+ "\"orientation\":\"orthogonal\", \"properties\": { }, \"tileheight\":32, "
+ "\"tilesets\":[ { \"firstgid\":1, \"image\":\"..\\/images\\/tiles.png\", "
+ "\"imageheight\":64, \"imagewidth\":160, \"margin\":0, \"name\":\"Tiles\", "
+ "\"properties\":{}, \"spacing\":0, \"tileheight\":32, \"tilewidth\":32 }], "
+ "\"tilewidth\":32, \"version\":1, \"width\":10 }";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), tokens, 128);
+ check(r >= 0);
+#if 0
+ for (i = 1; tokens[i].end < tokens[0].end; i++) {
+ if (tokens[i].type == JSMN_STRING || tokens[i].type == JSMN_PRIMITIVE) {
+ printf("%.*s\n", tokens[i].end - tokens[i].start, js + tokens[i].start);
+ } else if (tokens[i].type == JSMN_ARRAY) {
+ printf("[%d elems]\n", tokens[i].size);
+ } else if (tokens[i].type == JSMN_OBJECT) {
+ printf("{%d elems}\n", tokens[i].size);
+ } else {
+ TOKEN_PRINT(tokens[i]);
+ }
+ }
+#endif
+ return 0;
+}
+
+int test_unicode_characters() {
+ jsmn_parser p;
+ jsmntok_t tokens[10];
+ const char *js;
+
+ int r;
+ js = "{\"a\":\"\\uAbcD\"}";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), tokens, 10);
+ check(r >= 0);
+
+ js = "{\"a\":\"str\\u0000\"}";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), tokens, 10);
+ check(r >= 0);
+
+ js = "{\"a\":\"\\uFFFFstr\"}";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), tokens, 10);
+ check(r >= 0);
+
+ js = "{\"a\":\"str\\uFFGFstr\"}";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), tokens, 10);
+ check(r == JSMN_ERROR_INVAL);
+
+ js = "{\"a\":\"str\\u@FfF\"}";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), tokens, 10);
+ check(r == JSMN_ERROR_INVAL);
+
+ js = "{\"a\":[\"\\u028\"]}";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), tokens, 10);
+ check(r == JSMN_ERROR_INVAL);
+
+ js = "{\"a\":[\"\\u0280\"]}";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), tokens, 10);
+ check(r >= 0);
+
+ return 0;
+}
+
+int test_input_length() {
+ const char *js;
+ int r;
+ jsmn_parser p;
+ jsmntok_t tokens[10];
+
+ js = "{\"a\": 0}garbage";
+
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, 8, tokens, 10);
+ check(r == 3);
+ check(TOKEN_STRING(js, tokens[0], "{\"a\": 0}"));
+ check(TOKEN_STRING(js, tokens[1], "a"));
+ check(TOKEN_STRING(js, tokens[2], "0"));
+
+ return 0;
+}
+
+int test_count() {
+ jsmn_parser p;
+ const char *js;
+
+ js = "{}";
+ jsmn_init(&p);
+ check(jsmn_parse(&p, js, strlen(js), NULL, 0) == 1);
+
+ js = "[]";
+ jsmn_init(&p);
+ check(jsmn_parse(&p, js, strlen(js), NULL, 0) == 1);
+
+ js = "[[]]";
+ jsmn_init(&p);
+ check(jsmn_parse(&p, js, strlen(js), NULL, 0) == 2);
+
+ js = "[[], []]";
+ jsmn_init(&p);
+ check(jsmn_parse(&p, js, strlen(js), NULL, 0) == 3);
+
+ js = "[[], []]";
+ jsmn_init(&p);
+ check(jsmn_parse(&p, js, strlen(js), NULL, 0) == 3);
+
+ js = "[[], [[]], [[], []]]";
+ jsmn_init(&p);
+ check(jsmn_parse(&p, js, strlen(js), NULL, 0) == 7);
+
+ js = "[\"a\", [[], []]]";
+ jsmn_init(&p);
+ check(jsmn_parse(&p, js, strlen(js), NULL, 0) == 5);
+
+ js = "[[], \"[], [[]]\", [[]]]";
+ jsmn_init(&p);
+ check(jsmn_parse(&p, js, strlen(js), NULL, 0) == 5);
+
+ js = "[1, 2, 3]";
+ jsmn_init(&p);
+ check(jsmn_parse(&p, js, strlen(js), NULL, 0) == 4);
+
+ js = "[1, 2, [3, \"a\"], null]";
+ jsmn_init(&p);
+ check(jsmn_parse(&p, js, strlen(js), NULL, 0) == 7);
+
+ return 0;
+}
+
+int test_keyvalue() {
+ const char *js;
+ int r;
+ jsmn_parser p;
+ jsmntok_t tokens[10];
+
+ js = "{\"a\": 0, \"b\": \"c\"}";
+
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), tokens, 10);
+ check(r == 5);
+ check(tokens[0].size == 2); /* two keys */
+ check(tokens[1].size == 1 && tokens[3].size == 1); /* one value per key */
+ check(tokens[2].size == 0 && tokens[4].size == 0); /* values have zero size */
+
+ js = "{\"a\"\n0}";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), tokens, 10);
+ check(r == JSMN_ERROR_INVAL);
+
+ js = "{\"a\", 0}";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), tokens, 10);
+ check(r == JSMN_ERROR_INVAL);
+
+ js = "{\"a\": {2}}";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), tokens, 10);
+ check(r == JSMN_ERROR_INVAL);
+
+ js = "{\"a\": {2: 3}}";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), tokens, 10);
+ check(r == JSMN_ERROR_INVAL);
+
+
+ js = "{\"a\": {\"a\": 2 3}}";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), tokens, 10);
+ check(r == JSMN_ERROR_INVAL);
+ return 0;
+}
+
+/** A huge redefinition of everything to include jsmn in non-script mode */
+#define jsmn_init jsmn_init_nonstrict
+#define jsmn_parse jsmn_parse_nonstrict
+#define jsmn_parser jsmn_parser_nonstrict
+#define jsmn_alloc_token jsmn_alloc_token_nonstrict
+#define jsmn_fill_token jsmn_fill_token_nonstrict
+#define jsmn_parse_primitive jsmn_parse_primitive_nonstrict
+#define jsmn_parse_string jsmn_parse_string_nonstrict
+#define jsmntype_t jsmntype_nonstrict_t
+#define jsmnerr_t jsmnerr_nonstrict_t
+#define jsmntok_t jsmntok_nonstrict_t
+#define JSMN_PRIMITIVE JSMN_PRIMITIVE_NONSTRICT
+#define JSMN_OBJECT JSMN_OBJECT_NONSTRICT
+#define JSMN_ARRAY JSMN_ARRAY_NONSTRICT
+#define JSMN_STRING JSMN_STRING_NONSTRICT
+#define JSMN_ERROR_NOMEM JSMN_ERROR_NOMEM_NONSTRICT
+#define JSMN_ERROR_INVAL JSMN_ERROR_INVAL_NONSTRICT
+#define JSMN_ERROR_PART JSMN_ERROR_PART_NONSTRICT
+#undef __JSMN_H_
+#undef JSMN_STRICT
+#include "jsmn.c"
+
+int test_nonstrict() {
+ const char *js;
+ int r;
+ jsmn_parser p;
+ jsmntok_t tokens[10];
+
+ js = "a: 0garbage";
+
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, 4, tokens, 10);
+ check(r == 2);
+ check(TOKEN_STRING(js, tokens[0], "a"));
+ check(TOKEN_STRING(js, tokens[1], "0"));
+
+ js = "Day : 26\nMonth : Sep\n\nYear: 12";
+ jsmn_init(&p);
+ r = jsmn_parse(&p, js, strlen(js), tokens, 10);
+ check(r == 6);
+ return 0;
+}
+
+int main() {
+ test(test_empty, "general test for a empty JSON objects/arrays");
+ test(test_simple, "general test for a simple JSON string");
+ test(test_primitive, "test primitive JSON data types");
+ test(test_string, "test string JSON data types");
+ test(test_partial_string, "test partial JSON string parsing");
+ test(test_partial_array, "test partial array reading");
+ test(test_array_nomem, "test array reading with a smaller number of tokens");
+ test(test_unquoted_keys, "test unquoted keys (like in JavaScript)");
+ test(test_objects_arrays, "test objects and arrays");
+ test(test_unicode_characters, "test unicode characters");
+ test(test_input_length, "test strings that are not null-terminated");
+ test(test_issue_22, "test issue #22");
+ test(test_count, "test tokens count estimation");
+ test(test_nonstrict, "test for non-strict mode");
+ test(test_keyvalue, "test for keys/values");
+ printf("\nPASSED: %d\nFAILED: %d\n", test_passed, test_failed);
+ return 0;
+}
+
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) \
FILE *aud = NULL;
char b[PATH_MAX + 1];
char *maildir_copy = NULL; /* copy of mail directory because the static gets overwritten */
+ const char *oauth_svc = NULL;
int nmsgs, nbytes;
char *MAILHOST_env_variable;
-
done=inc_done;
/* absolutely the first thing we do is save our privileges,
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]);
if (inc_type == INC_POP) {
struct nmh_creds creds = { 0, 0, 0 };
+ if (oauth_svc == NULL) {
+ nmh_get_credentials (host, user, sasl, &creds);
+ } else {
+ if (user == NULL) {
+ adios (NULL, "must specify -user with -oauth");
+ }
+ creds.user = user;
+ }
+
/*
* initialize POP connection
*/
- nmh_get_credentials (host, user, sasl, &creds);
if (pop_init (host, port, creds.user, creds.password, proxy, snoop,
- sasl, saslmech) == NOTOK)
+ sasl, saslmech, oauth_svc) == NOTOK)
adios (NULL, "%s", response);
/* Check if there are any messages */
--- /dev/null
+/*
+ * 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
+}
"";
#endif
+static char *oauth =
+#ifdef OAUTH_SUPPORT
+ "oauth";
+#else
+ "";
+#endif
+
struct proc {
char *p_name;
char **p_field;
{ "datalocking", &datalocking },
{ "spoollocking", &spoollocking },
{ "iconv", &iconv },
+ { "oauth", &oauth },
{ "sasl", &sasl },
{ "tls", &tls },
{ NULL, NULL },
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,
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
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; }
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]);
if (host) {
if (vecp == 0) {
status = remotemail (host, port, user, proxy, notifysw, 1,
- snoop, sasl, saslmech);
+ snoop, sasl, saslmech, oauth_svc);
} else {
for (vecp = 0; vec[vecp]; vecp++)
status += remotemail (host, port, vec[vecp], proxy, notifysw, 0,
- snoop, sasl, saslmech);
+ snoop, sasl, saslmech, oauth_svc);
}
} else {
if (user == NULL) user = getusername ();
static int
remotemail (char *host, char *port, char *user, char *proxy, int notifysw,
- int personal, int snoop, int sasl, char *saslmech)
+ int personal, int snoop, int sasl, char *saslmech,
+ const char *oauth_svc)
{
int nmsgs, nbytes, status;
struct nmh_creds creds = { 0, 0, 0 };
+ if (oauth_svc == NULL) {
+ nmh_get_credentials (host, user, sasl, &creds);
+ } else {
+ if (user == NULL) {
+ adios (NULL, "must specify -user with -oauth");
+ }
+ creds.user = user;
+ }
+
/* open the POP connection */
- nmh_get_credentials (host, user, sasl, &creds);
if (pop_init (host, port, creds.user, creds.password, proxy, snoop, sasl,
- saslmech) == NOTOK
+ saslmech, oauth_svc) == NOTOK
|| pop_stat (&nmsgs, &nbytes) == NOTOK /* check for messages */
|| pop_quit () == NOTOK) { /* quit POP connection */
advise (NULL, "%s", response);
#include <h/mh.h>
#include <h/utils.h>
+#include <h/oauth.h>
#ifdef CYRUS_SASL
# include <sasl/sasl.h>
static int putline (char *, FILE *);
-#ifdef CYRUS_SASL
-/*
- * This function implements the AUTH command for various SASL mechanisms
- *
- * We do the whole SASL dialog here. If this completes, then we've
- * authenticated successfully and have (possibly) negotiated a security
- * layer.
- */
-
-#define CHECKB64SIZE(insize, outbuf, outsize) \
- { size_t wantout = (((insize + 2) / 3) * 4) + 32; \
- if (wantout > outsize) { \
- outbuf = mh_xrealloc(outbuf, outsize = wantout); \
- } \
- }
-
int
-pop_auth_sasl(char *user, char *host, char *mech)
+check_mech(char *server_mechs, size_t server_mechs_size, char *mech)
{
- int result, status, sasl_capability = 0;
- unsigned int buflen, outlen;
- char server_mechs[256], *buf, *outbuf = NULL;
- size_t outbufsize = 0;
- const char *chosen_mech;
- sasl_security_properties_t secprops;
- struct pass_context p_context;
- sasl_ssf_t *ssf;
- int *moutbuf;
+ int status, sasl_capability = 0;
/*
* First off, we're going to send the CAPA command to see if we can
* 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;
}
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.
*/
}
#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
int
pop_init (char *host, char *port, char *user, char *pass, char *proxy,
- int snoop, int sasl, char *mech)
+ int snoop, int sasl, char *mech, const char *oauth_svc)
{
int fd1, fd2;
char buffer[BUFSIZ];
+ const char *xoauth_client_res = NULL;
#ifndef CYRUS_SASL
NMH_UNUSED (sasl);
NMH_UNUSED (mech);
#endif /* ! CYRUS_SASL */
+#ifdef OAUTH_SUPPORT
+ if (oauth_svc != NULL) {
+ xoauth_client_res = mh_oauth_do_xoauth(user, oauth_svc,
+ snoop ? stderr : NULL);
+ }
+#else
+ NMH_UNUSED (oauth_svc);
+ NMH_UNUSED (xoauth_client_res);
+#endif /* OAUTH_SUPPORT */
+
if (proxy && *proxy) {
int pid;
int inpipe[2]; /* for reading from the server */
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)
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) \
static int annoaux (struct mailname *);
static void insert_fcc (struct headers *, char *);
static void make_bcc_file (int);
-static void verify_all_addresses (int, char *);
+static void verify_all_addresses (int, char *, const char *);
static void chkadr (void);
static void sigon (void);
static void sigoff (void);
static void p_refile (char *);
static void fcc (char *, char *);
static void die (char *, char *, ...);
-static void post (char *, int, int, char *);
+static void post (char *, int, int, char *, const char *);
static void do_text (char *file, int fd);
static void do_an_address (struct mailname *, int);
static void do_addresses (int, int);
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 (!(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]);
/* 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);
}
verify_all_addresses with MTS_SENDMAIL_PIPE, but
that might require running sendmail as root. Note
that spost didn't verify addresses. */
- verify_all_addresses (verbose, envelope);
+ verify_all_addresses (verbose, envelope, xoauth_client_res);
}
- post (tmpfil, 0, verbose, envelope);
+ post (tmpfil, 0, verbose, envelope, xoauth_client_res);
}
- post (bccfil, 1, verbose, envelope);
+ post (bccfil, 1, verbose, envelope, xoauth_client_res);
(void) m_unlink (bccfil);
} else {
- post (tmpfil, 0, isatty (1), envelope);
+ post (tmpfil, 0, isatty (1), envelope, xoauth_client_res);
}
p_refile (tmpfil);
*/
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;
} 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);
/* 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;
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));
#include <h/mh.h>
#include <fcntl.h>
+#include <h/oauth.h>
+#include <h/utils.h>
#ifndef CYRUS_SASL
# define SASLminc(a) (a)
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) \
char *cp, *dfolder = NULL, *maildir = NULL;
char buf[BUFSIZ], **ap, **argp, **arguments, *program;
char *msgs[MAXARGS], **vec;
+ const char *user = NULL, *oauth_svc = NULL;
struct msgs *mp;
struct stat st;
+ int snoop = 0;
if (nmh_init(argv[0], 1)) { return 1; }
vec[vecp++] = --cp;
continue;
+ case SNOOPSW:
+ snoop++;
+ vec[vecp++] = --cp;
+ continue;
+
case DEBUGSW:
debugsw++; /* fall */
case NFILTSW:
case NMSGDSW:
case WATCSW:
case NWATCSW:
- case SNOOPSW:
case SASLSW:
case NOSASLSW:
case TLSSW:
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 SERVSW:
case SASLMECHSW:
case SASLMXSSFSW:
- case USERSW:
case PORTSW:
case MTSSM:
case MTSSW:
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;