Big patch: Add XOAUTH2 support for SMTP and POP

Previous Topic Next Topic
 
classic Classic list List threaded Threaded
17 messages Options
Reply | Threaded
Open this post in threaded view
|

Big patch: Add XOAUTH2 support for SMTP and POP

Eric Gillespie
Background: http://googleappsdeveloper.blogspot.com/2014/10/updates-on-authentication-for-gmail.html

Unless you use two-step verification and create an app password
(https://support.google.com/accounts/answer/185833), or are
willing to enable these risky logins, you can no longer use nmh
with Gmail for POP and SMTP.

I guess this is a bit of a distraction from the current focus on
improving MIME support and such, and I'm not helping with that
at all.  Sorry; this is a blocker for me, that's not :(

Disclosure: I work at Google in the same group that works on
Google OAuth support, though I work on other parts of the
identity infrastructure.  Aside from the only hinted at POP
support, this is all thoroughly covered by public documentation;
I don't have any insider OAuth knowledge.

OAuth is a very modern thing to bring into nmh.  You need to be
able to make HTTP requests and process JSON resonses.  Luckily,
curl is nearly ubiquitous.  This does not seem to be the case for
any JSON library, nor does there even seem to be one obvious
implementation to go with.  I picked jsmn because it has a
liberal license and is tiny (only 312 lines).

I enable a libcurl dependency only when configured with
--with-oauth which is off by default.  But practically no one has
jsmn installed, so I'm suggesting we include it directly.
I think that might be unprecedented for nmh.  But I hope it's not
too controversial.

I updated the man pages for send, inc, and msgchk, but it's
probably not good enough; suggestions welcome.

Quick overview:

% mhlogin -oauth gmail
Load the following URL in your browser and authorize nmh to access Gmail:

https://accounts.google.com/o/oauth2/auth?response_type=code&client_id=91584523849-8lv9kgp1rvp8ahta6fa4b125tn2polcg.apps.googleusercontent.com&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&scope=https%3A%2F%2Fmail.google.com%2F

Enter the authorization code: 4/SJ-cCVsTYuRYXg9hHnzNJ8lRQbhNx-h8N6K0hr8ZM1o.8mV7QHLMqOAbEnp6UAPFm0Hc5WAelAI

% msgchk
[hidden email] has 1 message (5661 bytes) on localhost

% grep oauth .mh_profile
send: -server smtp.gmail.com -tls -oauth gmail -user [hidden email]
inc: -oauth gmail -host localhost -port 995 -user [hidden email]
msgchk: -oauth gmail -host localhost -port 995 -user [hidden email]

You have to use inc/msgchk -proxy because Gmail requires TLS.
For future reference: libcurl can handle POP and SMTP with TLS
for us too... :)

Tested with AddressSanitizer:
CFLAGS='-g -fno-omit-frame-pointer -fsanitize=address -static-libasan'

Unresolved issues:

- mhlogin name / flag names

  Naming is hard :).  I picked this on the theory that it's not
  terribly confusing as is, and if there were to be some other
  kind of system users might need to login to, expanding mhlogin
  to have more than just -oauth would make sense.

- Repeating -user for each command is possibly odd.  Maybe put
  -user on mhlogin and save it in the cred file.  Arguably easier
  -for the user this way, arguably not.  Changing it would
  -complicate the code slightly.  I don't really care either way.

- I have a lot of test cases in only a few broadly categorized
  test scripts, and they print descriptions as they go so it's
  easy to see what broke.  This messes up the test suite output.
  Does this make sense, should I change this only to print only
  if some environment variable is set, or should I just break
  these up into one test case per script?  I'd kinda prefer the
  latter, but I don't know if anyone objects to a big pile of
  test scripts in there.

- Owning the Google client credentials.  I took the liberty of
  creating a Google developer project for nmh already.  I'm happy
  to share ownership, or just give it away, or dispose of mine in
  favor of another.  I would suggest that the Google project have
  at least two owners.

Of course, I welcome criticism on all other aspects too:  API,
documentation, organization, whatever.

Thanks!



From 76f77f96a86d6211c664e22455fed26120f4529a Mon Sep 17 00:00:00 2001
From: Eric Gillespie <[hidden email]>
Date: Thu, 4 Dec 2014 23:47:08 -0800
Subject: [PATCH 1/2] import jsmn 86:19001fb4adb3

---
 thirdparty/jsmn/.hg_archival.txt   |   5 +
 thirdparty/jsmn/LICENSE            |  20 ++
 thirdparty/jsmn/Makefile           |  35 +++
 thirdparty/jsmn/README.md          | 165 ++++++++++
 thirdparty/jsmn/example/jsondump.c | 112 +++++++
 thirdparty/jsmn/example/simple.c   |  75 +++++
 thirdparty/jsmn/jsmn.c             | 311 +++++++++++++++++++
 thirdparty/jsmn/jsmn.h             |  75 +++++
 thirdparty/jsmn/jsmn_test.c        | 608 +++++++++++++++++++++++++++++++++++++
 9 files changed, 1406 insertions(+)
 create mode 100644 thirdparty/jsmn/.hg_archival.txt
 create mode 100644 thirdparty/jsmn/LICENSE
 create mode 100644 thirdparty/jsmn/Makefile
 create mode 100644 thirdparty/jsmn/README.md
 create mode 100644 thirdparty/jsmn/example/jsondump.c
 create mode 100644 thirdparty/jsmn/example/simple.c
 create mode 100644 thirdparty/jsmn/jsmn.c
 create mode 100644 thirdparty/jsmn/jsmn.h
 create mode 100644 thirdparty/jsmn/jsmn_test.c

diff --git a/thirdparty/jsmn/.hg_archival.txt b/thirdparty/jsmn/.hg_archival.txt
new file mode 100644
index 0000000..749f61c
--- /dev/null
+++ b/thirdparty/jsmn/.hg_archival.txt
@@ -0,0 +1,5 @@
+repo: 90642b15dce19adbb69f2b1792b263209079ce4b
+node: 19001fb4adb3914cd4f7584c828fa2e0ff7f4922
+branch: default
+latesttag: null
+latesttagdistance: 79
diff --git a/thirdparty/jsmn/LICENSE b/thirdparty/jsmn/LICENSE
new file mode 100644
index 0000000..c84fb2e
--- /dev/null
+++ b/thirdparty/jsmn/LICENSE
@@ -0,0 +1,20 @@
+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.
+
diff --git a/thirdparty/jsmn/Makefile b/thirdparty/jsmn/Makefile
new file mode 100644
index 0000000..5e3e2a9
--- /dev/null
+++ b/thirdparty/jsmn/Makefile
@@ -0,0 +1,35 @@
+# 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
+
diff --git a/thirdparty/jsmn/README.md b/thirdparty/jsmn/README.md
new file mode 100644
index 0000000..a282407
--- /dev/null
+++ b/thirdparty/jsmn/README.md
@@ -0,0 +1,165 @@
+
+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
diff --git a/thirdparty/jsmn/example/jsondump.c b/thirdparty/jsmn/example/jsondump.c
new file mode 100644
index 0000000..3490bbf
--- /dev/null
+++ b/thirdparty/jsmn/example/jsondump.c
@@ -0,0 +1,112 @@
+#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;
+}
diff --git a/thirdparty/jsmn/example/simple.c b/thirdparty/jsmn/example/simple.c
new file mode 100644
index 0000000..a6f8e6a
--- /dev/null
+++ b/thirdparty/jsmn/example/simple.c
@@ -0,0 +1,75 @@
+#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;
+}
diff --git a/thirdparty/jsmn/jsmn.c b/thirdparty/jsmn/jsmn.c
new file mode 100644
index 0000000..a0f4f69
--- /dev/null
+++ b/thirdparty/jsmn/jsmn.c
@@ -0,0 +1,311 @@
+#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;
+}
+
diff --git a/thirdparty/jsmn/jsmn.h b/thirdparty/jsmn/jsmn.h
new file mode 100644
index 0000000..95fb2ca
--- /dev/null
+++ b/thirdparty/jsmn/jsmn.h
@@ -0,0 +1,75 @@
+#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_ */
diff --git a/thirdparty/jsmn/jsmn_test.c b/thirdparty/jsmn/jsmn_test.c
new file mode 100644
index 0000000..3968859
--- /dev/null
+++ b/thirdparty/jsmn/jsmn_test.c
@@ -0,0 +1,608 @@
+#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;
+}
+
--
2.1.3



From c930bcae84a82536c5092ea7608922056c80e96c Mon Sep 17 00:00:00 2001
From: Eric Gillespie <[hidden email]>
Date: Thu, 4 Dec 2014 23:53:44 -0800
Subject: [PATCH 2/2] Implement OAuth 2.0 [1] for XOAUTH2 in SMTP [2] and POP3
 [3].

Google defined XOAUTH2 for SMTP, and that's what we use here.  If other
providers implement XOAUTH2 or some similar OAuth-based SMTP authentication
protocol, it should be simple to extend this.

[1] https://tools.ietf.org/html/rfc6749
[2] https://developers.google.com/gmail/xoauth2_protocol
[3] http://googleappsdeveloper.blogspot.com/2014/10/updates-on-authentication-for-gmail.html

Technically, XOAUTH2 is a SASL auth mechanism, but the implementation is so
trivial, I can't justify the code complexity or additional dependency
requirement of using Cyrus SASL for this.  So it's completely separate.

Changes:

- New dependencies:

  - jsmn (JSON processing library) bundled directly rather than linked to as
    an external library because there is no clear winner among JSON
    libraries for C and this one is tiny

  - libcurl is nearly ubiquitous and too heavy-weight to bundle, so link to
    the library the user must install separately

- Add oauth.h / oauth.c which do almost all the work, with quite a bit of
  help from curl and jsmn.

- Add new mhlogin program to authorize nmh to use the Gmail account and
  store the access and refresh tokens.

- Add new user_agent global to version.c (version.sh); not too happy with
  such a generic name, but the others had no mh_ prefix or anything...

- Add XOAUTH2 support to:
  mts/smtp/smtp.c uip/post.c uip/send.c uip/popsbr.c uip/inc.c uip/msgchk.c

- Split duplicated serving code out of fakepop.c and fakesmtp.c to new
  server.c and also use that for new fakehttp.c.

- Add XOAUTH2 support to fakepop.c and fakesmtp.c.
---
 .gitignore              |    2 +
 Makefile.am             |   39 +-
 config/version.sh       |    1 +
 configure.ac            |   26 ++
 h/mh.h                  |    1 +
 h/oauth.h               |  221 +++++++++
 h/popsbr.h              |    3 +-
 h/prototypes.h          |    6 +-
 man/inc.man             |   32 +-
 man/mhlogin.man         |   63 +++
 man/msgchk.man          |   33 +-
 man/send.man            |   27 +-
 mts/smtp/smtp.c         |   62 ++-
 mts/smtp/smtp.h         |    3 +-
 sbr/error.c             |    4 +-
 sbr/oauth.c             | 1158 +++++++++++++++++++++++++++++++++++++++++++++++
 sbr/readconfig.c        |    2 +-
 test/fakehttp.c         |  126 ++++++
 test/fakepop.c          |  279 +++---------
 test/fakesmtp.c         |  270 +++--------
 test/oauth/common.sh    |  173 +++++++
 test/oauth/test-inc     |  116 +++++
 test/oauth/test-mhlogin |  257 +++++++++++
 test/oauth/test-send    |  357 +++++++++++++++
 test/oauth/test-share   |  139 ++++++
 test/server.c           |  248 ++++++++++
 uip/inc.c               |   25 +-
 uip/mhlogin.c           |  162 +++++++
 uip/msgchk.c            |   33 +-
 uip/popsbr.c            |  106 +++--
 uip/post.c              |   35 +-
 uip/send.c              |   43 +-
 32 files changed, 3521 insertions(+), 531 deletions(-)
 create mode 100644 h/oauth.h
 create mode 100644 man/mhlogin.man
 create mode 100644 sbr/oauth.c
 create mode 100644 test/fakehttp.c
 create mode 100644 test/oauth/common.sh
 create mode 100755 test/oauth/test-inc
 create mode 100755 test/oauth/test-mhlogin
 create mode 100755 test/oauth/test-send
 create mode 100755 test/oauth/test-share
 create mode 100644 test/server.c
 create mode 100644 uip/mhlogin.c

diff --git a/.gitignore b/.gitignore
index 4e9297d..2703810 100644
--- a/.gitignore
+++ b/.gitignore
@@ -80,6 +80,7 @@ a.out.dSYM/
 /uip/mhfixmsg
 /uip/mhl
 /uip/mhlist
+/uip/mhlogin
 /uip/mhn
 /uip/mhparam
 /uip/mhpath
@@ -110,6 +111,7 @@ a.out.dSYM/
 /uip/whatnow
 /uip/whom
 /uip/*.exe
+/test/fakehttp
 /test/fakepop
 /test/fakesmtp
 /test/getcanon
diff --git a/Makefile.am b/Makefile.am
index 1cc0875..59269d5 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -34,6 +34,9 @@ nmhlibexecdir = @libexecdir@/nmh
 ## nmh _does_ have a test suite!
 ##
 TESTS_ENVIRONMENT = MH_OBJ_DIR="@abs_builddir@" \
+    MH_VERSION="$(VERSION)" \
+    OAUTH_SUPPORT='@OAUTH_SUPPORT@' \
+    CURL_USER_AGENT='@CURL_USER_AGENT@' \
     MH_TEST_DIR="@abs_builddir@/test/testdir" \
     nmhlibexecdir="$(nmhlibexecdir)" bindir="$(bindir)" \
     mandir="$(mandir)" nmhetcdir="$(nmhetcdir)" \
@@ -81,6 +84,8 @@ TESTS = test/ali/test-ali test/anno/test-anno \
  test/mhshow/test-subpart test/mhshow/test-msg-buffer-boundaries \
  test/mhstore/test-mhstore test/mkstemp/test-mkstemp \
  test/new/test-basic test/pick/test-pick test/pick/test-stderr \
+ test/oauth/test-mhlogin test/oauth/test-send \
+ test/oauth/test-inc test/oauth/test-share \
  test/post/test-post-aliases test/post/test-post-basic \
  test/post/test-post-multiple test/post/test-post-bcc \
  test/post/test-post-dcc test/post/test-post-fcc \
@@ -101,7 +106,7 @@ TESTS = test/ali/test-ali test/anno/test-anno \
 
 check_SCRIPTS = test/common.sh
 check_PROGRAMS = test/getfullname test/getcanon test/fakepop test/fakesmtp \
- test/getcwidth
+ test/getcwidth test/fakehttp
 DISTCHECK_CONFIGURE_FLAGS = DISABLE_SETGID_MAIL=1
 
 ##
@@ -147,7 +152,7 @@ bin_PROGRAMS = uip/ali uip/anno uip/burst uip/comp uip/dist uip/flist \
        uip/mhparam uip/mhpath uip/mhshow uip/mhstore uip/msgchk \
        uip/new uip/packf uip/pick uip/prompter uip/refile \
        uip/repl uip/rmf uip/rmm uip/scan uip/send uip/show uip/sortm \
-       uip/whatnow uip/whom
+       uip/whatnow uip/whom uip/mhlogin
 
 bin_SCRIPTS = uip/mhmail etc/sendfiles
 
@@ -182,7 +187,8 @@ noinst_HEADERS = h/addrsbr.h h/aliasbr.h h/crawl_folders.h h/dropsbr.h \
  h/mh.h h/mhcachesbr.h h/mhparse.h h/mime.h \
  h/mts.h h/nmh.h h/picksbr.h h/popsbr.h h/prototypes.h \
  h/rcvmail.h h/scansbr.h h/signals.h h/tws.h h/utils.h \
- mts/smtp/smtp.h sbr/ctype-checked.h
+ mts/smtp/smtp.h sbr/ctype-checked.h h/oauth.h \
+ thirdparty/jsmn/jsmn.h
 
 ##
 ## Extra files we need to install in various places
@@ -239,7 +245,7 @@ man_MANS = man/ali.1 man/anno.1 man/ap.8 man/burst.1 man/comp.1 \
    man/prompter.1 man/rcvdist.1 man/rcvpack.1 man/rcvstore.1 \
    man/rcvtty.1 man/refile.1 man/repl.1 man/rmf.1 man/rmm.1 \
    man/scan.1 man/send.1 man/sendfiles.1 man/show.1 man/slocal.1 \
-   man/sortm.1 man/unseen.1 man/whatnow.1 man/whom.1
+   man/sortm.1 man/unseen.1 man/whatnow.1 man/whom.1 man/mhlogin.1
 
 ##
 ## Sources for our man pages
@@ -261,7 +267,7 @@ man_SRCS = man/ali.man man/anno.man man/ap.man man/burst.man man/comp.man \
    man/rcvstore.man man/rcvtty.man man/refile.man man/repl.man \
    man/rmf.man man/rmm.man man/scan.man man/send.man \
    man/sendfiles.man man/show.man man/slocal.man man/sortm.man \
-   man/unseen.man man/whatnow.man man/whom.man
+   man/unseen.man man/whatnow.man man/whom.man man/mhlogin.man
 
 ##
 ## Files we need to include in the distribution which aren't found by
@@ -278,7 +284,9 @@ EXTRA_DIST = autogen.sh config/version.sh sbr/sigmsg.awk etc/mts.conf.in \
      test/mhbuild/somebinary \
      test/mhbuild/nulls \
      test/mhbuild/textplain \
-     test/post/test-post-common.sh test/valgrind.supp uip/mhmail \
+     test/post/test-post-common.sh test/valgrind.supp \
+     test/oauth/common.sh \
+     uip/mhmail \
      SPECS/nmh.spec SPECS/build-nmh-cygwin $(man_SRCS)
 
 ##
@@ -318,7 +326,7 @@ uip_forw_SOURCES = uip/forw.c uip/whatnowproc.c uip/whatnowsbr.c uip/sendsbr.c \
 uip_forw_LDADD = $(LDADD) $(READLINELIB) $(TERMLIB) $(ICONVLIB) $(POSTLINK)
 
 uip_inc_SOURCES = uip/inc.c uip/scansbr.c uip/dropsbr.c uip/popsbr.c
-uip_inc_LDADD = $(LDADD) $(TERMLIB) $(ICONVLIB) $(SASLLIB) $(POSTLINK)
+uip_inc_LDADD = $(LDADD) $(TERMLIB) $(ICONVLIB) $(SASLLIB) $(POSTLINK) $(CURLLIB)
 
 uip_install_mh_SOURCES = uip/install-mh.c
 uip_install_mh_LDADD = $(LDADD) $(POSTLINK)
@@ -362,7 +370,7 @@ uip_mhstore_SOURCES = uip/mhstore.c uip/mhparse.c uip/mhcachesbr.c \
 uip_mhstore_LDADD = $(LDADD) $(TERMLIB) $(ICONVLIB) $(POSTLINK)
 
 uip_msgchk_SOURCES = uip/msgchk.c uip/popsbr.c
-uip_msgchk_LDADD = $(LDADD) $(SASLLIB) $(POSTLINK)
+uip_msgchk_LDADD = $(LDADD) $(SASLLIB) $(POSTLINK) $(CURLLIB)
 
 uip_new_SOURCES = uip/new.c
 uip_new_LDADD = $(LDADD) $(POSTLINK)
@@ -394,7 +402,7 @@ uip_scan_LDADD = $(LDADD) $(TERMLIB) $(ICONVLIB) $(POSTLINK)
 
 uip_send_SOURCES = uip/send.c uip/sendsbr.c uip/annosbr.c \
    uip/distsbr.c
-uip_send_LDADD = $(LDADD) $(POSTLINK)
+uip_send_LDADD = $(LDADD) $(POSTLINK) $(CURLLIB)
 
 uip_show_SOURCES = uip/show.c uip/mhlsbr.c
 uip_show_LDADD = $(LDADD) $(TERMLIB) $(ICONVLIB) $(POSTLINK)
@@ -428,6 +436,9 @@ uip_fmttest_LDADD = $(LDADD) $(TERMLIB) $(ICONVLIB) $(POSTLINK)
 uip_mhl_SOURCES = uip/mhl.c uip/mhlsbr.c
 uip_mhl_LDADD = $(LDADD) $(TERMLIB) $(ICONVLIB) $(POSTLINK)
 
+uip_mhlogin_SOURCES = uip/mhlogin.c
+uip_mhlogin_LDADD = mts/libmts.a $(LDADD) $(CURLLIB) $(POSTLINK)
+
 uip_mkstemp_SOURCES = uip/mkstemp.c
 uip_mkstemp_LDADD = $(LDADD) $(POSTLINK)
 
@@ -459,12 +470,15 @@ test_getfullname_LDADD = $(LDADD) $(POSTLINK)
 test_getcanon_SOURCES = test/getcanon.c
 test_getcanon_LDADD = $(POSTLINK)
 
-test_fakepop_SOURCES = test/fakepop.c
+test_fakepop_SOURCES = test/fakepop.c test/server.c
 test_fakepop_LDADD = $(POSTLINK)
 
-test_fakesmtp_SOURCES = test/fakesmtp.c
+test_fakesmtp_SOURCES = test/fakesmtp.c test/server.c
 test_fakesmtp_LDADD = $(POSTLINK)
 
+test_fakehttp_SOURCES = test/fakehttp.c test/server.c
+test_fakehttp_LDADD = $(POSTLINK)
+
 test_getcwidth_SOURCES = test/getcwidth.c
 test_getcwidth_LDADD = $(POSTLINK)
 
@@ -587,7 +601,8 @@ sbr_libmh_a_SOURCES = sbr/addrsbr.c sbr/ambigsw.c sbr/atooi.c sbr/arglist.c \
       sbr/uprf.c sbr/vfgets.c \
       sbr/mf.c sbr/utils.c sbr/ctype-checked.c \
       sbr/m_mktemp.c sbr/getansreadline.c sbr/vector.c \
-      config/config.c config/version.c
+      config/config.c config/version.c sbr/oauth.c \
+      thirdparty/jsmn/jsmn.c
 
 ##
 ## Because these files use the definitions in the libmh rule below,
diff --git a/config/version.sh b/config/version.sh
index bf95447..a0105e3 100755
--- a/config/version.sh
+++ b/config/version.sh
@@ -47,3 +47,4 @@ else
     echo "char *version_str = \"nmh-$VERSION [compiled on $HOSTNAME at `date`]\";"
 fi
 echo "char *version_num = \"nmh-$VERSION\";"
+echo "char *user_agent = \"nmh/$VERSION\";"
diff --git a/configure.ac b/configure.ac
index e0a96f5..507e4ea 100644
--- a/configure.ac
+++ b/configure.ac
@@ -40,6 +40,15 @@ AS_IF([test x"$with_cyrus_sasl" != x -a x"$with_cyrus_sasl" != x"no"],[
     AC_MSG_WARN([Please pass the appropriate arguments to CPPFLAGS/LDFLAGS])])
       sasl_support=yes], [sasl_support=no])
 
+dnl Do you want client-side support for using OAuth2 for SMTP authentication?
+AC_ARG_WITH([oauth], AS_HELP_STRING([--with-oauth],
+  [Enable OAuth2 support in SMTP auth]))
+AS_IF([test x"$with_oauth" != x -a x"$with_oauth" != x"no"],[
+      AC_DEFINE([OAUTH_SUPPORT], [1],
+ [Support OAuth2 in SMTP auth.])dnl
+      OAUTH_SUPPORT=1; oauth_support=yes], [OAUTH_SUPPORT=0; oauth_support=no])
+AC_SUBST(OAUTH_SUPPORT)
+
 dnl Do you want client-side support for encryption with TLS?
 AC_ARG_WITH([tls], AS_HELP_STRING([--with-tls], [Enable TLS support]))
 AS_IF([test x"$with_tls" != x"no"],[
@@ -501,6 +510,22 @@ AS_IF([test x"$tls_support" = x"yes"],[
   [TLSLIB=])
 AC_SUBST([TLSLIB])
 
+dnl -----------------
+dnl CHECK FOR CURL
+dnl -----------------
+AS_IF([test x"$OAUTH_SUPPORT" = x"1"],[
+  AC_PATH_PROG([curl_config], [curl-config])
+  AC_CHECK_HEADER([curl/curl.h], [], [AC_MSG_ERROR([curl/curl.h not found])])
+  AC_CHECK_LIB([curl], [curl_easy_init], [CURLLIB="`$curl_config --libs`"],
+    [AC_MSG_ERROR([curl library not found])],[$CURLLIB])
+  CURL_USER_AGENT=`$curl_config --version | sed 's| |/|'`
+  ],
+  [CURLLIB=
+   CURL_USER_AGENT=
+])
+AC_SUBST([CURLLIB])
+AC_SUBST([CURL_USER_AGENT])
+
 dnl ----------------
 dnl CHECK FLEX FIXUP
 dnl ----------------
@@ -590,6 +615,7 @@ spool default locking type : ${with_locking}
 default smtp servers       : ${smtpservers}
 SASL support               : ${sasl_support}
 TLS support                : ${tls_support}
+OAuth support              : ${oauth_support}
 ])])dnl
 
 dnl ---------------
diff --git a/h/mh.h b/h/mh.h
index b8c60ca..71315b6 100644
--- a/h/mh.h
+++ b/h/mh.h
@@ -490,6 +490,7 @@ extern char *sendproc;
 extern char *showmimeproc;
 extern char *showproc;
 extern char *usequence;
+extern char *user_agent;
 extern char *version_num;
 extern char *version_str;
 extern char *whatnowproc;
diff --git a/h/oauth.h b/h/oauth.h
new file mode 100644
index 0000000..a49cb4a
--- /dev/null
+++ b/h/oauth.h
@@ -0,0 +1,221 @@
+/*
+ * Implementation of OAuth 2.0 [1] for XOAUTH2 in SMTP [2] and POP3 [3].
+ *
+ * Google defined XOAUTH2 for SMTP, and that's what we use here.  If other
+ * providers implement XOAUTH2 or some similar OAuth-based SMTP authentication
+ * protocol, it should be simple to extend this.
+ *
+ * [1] https://tools.ietf.org/html/rfc6749
+ * [2] https://developers.google.com/gmail/xoauth2_protocol
+ * [3] http://googleappsdeveloper.blogspot.com/2014/10/updates-on-authentication-for-gmail.html
+ *
+ * Presumably [2] should document POP3 and that is an over-sight.  As it stands,
+ * that blog post is the closest we have to documentation.
+ *
+ * According to [1] 2.1 Client Types, this is a "native application", a
+ * "public" client.
+ *
+ * To summarize the flow:
+ *
+ * 1. User runs mhlogin which prints a URL the user must visit, and prompts for
+ *    a code retrieved from that page.
+ *
+ * 2. User vists this URL in browser, signs in with some Google account, and
+ *    copies and pastes the resulting code back to mhlogin.
+ *
+ * 3. mhlogin does HTTP POST to Google to exchange the user-provided code for a
+ *    short-lived access token and a long-lived refresh token.
+ *
+ * 4. send uses the access token in SMTP auth if not expired.  If it is expired,
+ *    it does HTTP POST to Google including the refresh token and gets back a
+ *    new access token (and possibly refresh token).  If the refresh token has
+ *    become invalid (e.g. if the user took some reset action on the Google
+ *    account), the user must use mhlogin again, then re-run send.
+ */
+
+typedef enum {
+    /* error loading profile */
+    MH_OAUTH_BAD_PROFILE = OK + 1,
+
+    /* error initializing libcurl */
+    MH_OAUTH_CURL_INIT,
+
+    /* local error initializing HTTP request */
+    MH_OAUTH_REQUEST_INIT,
+
+    /* error executing HTTP POST request */
+    MH_OAUTH_POST,
+
+    /* HTTP response body is too big. */
+    MH_OAUTH_RESPONSE_TOO_BIG,
+
+    /* Can't process HTTP response body. */
+    MH_OAUTH_RESPONSE_BAD,
+
+    /* The authorization server rejected the grant (authorization code or
+     * refresh token); possibly the user entered a bad code, or the refresh
+     * token has become invalid, etc. */
+    MH_OAUTH_BAD_GRANT,
+
+    /* HTTP server indicates something is wrong with our request. */
+    MH_OAUTH_REQUEST_BAD,
+
+    /* Attempting to refresh an access token without a refresh token. */
+    MH_OAUTH_NO_REFRESH,
+
+    /* error loading serialized credentials */
+    MH_OAUTH_CRED_FILE
+} mh_oauth_err_code;
+
+typedef struct mh_oauth_ctx mh_oauth_ctx;
+
+typedef struct mh_oauth_cred mh_oauth_cred;
+
+/*
+ * Do the complete dance for XOAUTH2 as used by POP3 and SMTP.
+ *
+ * Load tokens for svc from disk, refresh if necessary, and return the
+ * base64-encoded client response.
+ *
+ * If refreshing, writes freshened tokens to disk.
+ *
+ * Exits via adios on any error.
+ */
+char *
+mh_oauth_do_xoauth(const char *user, const char *svc, FILE *log);
+
+/*
+ * Allocate and initialize a new OAuth context.
+ *
+ * Caller must call mh_oauth_free(ctx) when finished, even on error.
+ *
+ * svc_name must point to a null-terminated string identifying the service
+ * provider.  Support for "gmail" is built-in; anything else must be defined in
+ * the user's profile.  The profile can also override "gmail" settings.
+ *
+ * Accesses global m_defs via context_find.
+ *
+ * On error, return FALSE and set an error in ctx; ctx is always allocated.
+ */
+boolean
+mh_oauth_new(mh_oauth_ctx **ctx, const char *svc_name);
+
+/*
+ * Free all resources associated with ctx.
+ */
+void
+mh_oauth_free(mh_oauth_ctx *ctx);
+
+/*
+ * Return null-terminated human-readable name of the service, e.g. "Gmail".
+ *
+ * Never returns NULL.
+ */
+const char *
+mh_oauth_svc_display_name(const mh_oauth_ctx *ctx);
+
+/*
+ * Enable logging for subsequent operations on ctx.
+ *
+ * log must not be closed until after mh_oauth_free.
+ *
+ * For all HTTP requests, the request is logged with each line prefixed with
+ * "< ", and the response with "> ".  Other messages are prefixed with "* ".
+ */
+void
+mh_oauth_log_to(FILE *log, mh_oauth_ctx *ctx);
+
+/*
+ * Return the error code after some function indicated an error.
+ *
+ * Must not be called if an error was not indicated.
+ */
+mh_oauth_err_code
+mh_oauth_get_err_code(const mh_oauth_ctx *ctx);
+
+/*
+ * Return null-terminated error message after some function indicated an error.
+ *
+ * Never returns NULL, but must not be called if an error was not indicated.
+ */
+const char *
+mh_oauth_get_err_string(mh_oauth_ctx *ctx);
+
+/*
+ * Return the null-terminated URL the user needs to visit to authorize access.
+ *
+ * URL may be invalidated by subsequent calls to mh_oauth_get_authorize_url,
+ * mh_oauth_authorize, or mh_oauth_refresh.
+ *
+ * On error, return NULL.
+ */
+const char *
+mh_oauth_get_authorize_url(mh_oauth_ctx *ctx);
+
+/*
+ * Exchange code provided by the user for access (and maybe refresh) token.
+ *
+ * On error, return NULL.
+ */
+mh_oauth_cred *
+mh_oauth_authorize(const char *code, mh_oauth_ctx *ctx);
+
+/*
+ * Refresh access (and maybe refresh) token if refresh token present.
+ *
+ * On error, return FALSE and leave cred untouched.
+ */
+boolean
+mh_oauth_refresh(mh_oauth_cred *cred);
+
+/*
+ * Return whether access token is present and not expired at time T.
+ */
+boolean
+mh_oauth_access_token_valid(time_t t, const mh_oauth_cred *cred);
+
+/*
+ * Free all resources associated with cred.
+ */
+void
+mh_oauth_cred_free(mh_oauth_cred *cred);
+
+/*
+ * Return the null-terminated file name for storing this service's OAuth tokens.
+ *
+ * Accesses global m_defs via context_find.
+ *
+ * Never returns NULL.
+ */
+const char *
+mh_oauth_cred_fn(mh_oauth_ctx *ctx);
+
+/*
+ * Serialize OAuth tokens to file.
+ *
+ * On error, return FALSE.
+ */
+boolean
+mh_oauth_cred_save(FILE *fp, mh_oauth_cred *cred);
+
+/*
+ * Load OAuth tokens from file.
+ *
+ * Calls m_getfld(), which writes to stderr with advise().
+ *
+ * On error, return NULL.
+ */
+mh_oauth_cred *
+mh_oauth_cred_load(FILE *fp, mh_oauth_ctx *ctx);
+
+/*
+ * Return null-terminated SASL client response for XOAUTH2 from access token.
+ *
+ * Store the length in res_len.
+ *
+ * Must not be called except after successful mh_oauth_access_token_valid or
+ * mh_oauth_refresh call; i.e. must have a valid access token.
+ */
+const char *
+mh_oauth_sasl_client_response(size_t *res_len,
+                              const char *user, const mh_oauth_cred *cred);
diff --git a/h/popsbr.h b/h/popsbr.h
index fc06f0b..3fb4179 100644
--- a/h/popsbr.h
+++ b/h/popsbr.h
@@ -3,7 +3,8 @@
  * popsbr.h -- header for POP client subroutines
  */
 
-int pop_init (char *, char *, char *, char *, char *, int, int, char *);
+int pop_init (char *, char *, char *, char *, char *, int, int, char *,
+              const char *);
 int pop_fd (char *, int, char *, int);
 int pop_stat (int *, int *);
 int pop_retr (int, int (*)(char *));
diff --git a/h/prototypes.h b/h/prototypes.h
index a338fd5..5c104a7 100644
--- a/h/prototypes.h
+++ b/h/prototypes.h
@@ -18,9 +18,9 @@ char *etcpath(char *);
 struct msgs_array;
 
 void add_profile_entry (const char *, const char *);
-void adios (char *, char *, ...) NORETURN;
+void adios (char *, const char *, ...) NORETURN;
 void admonish (char *, char *, ...);
-void advertise (char *, char *, char *, va_list);
+void advertise (char *, char *, const char *, va_list);
 void advise (char *, char *, ...);
 char **argsplit (char *, char **, int *);
 void argsplit_msgarg (struct msgs_array *, char *, char **);
@@ -287,7 +287,7 @@ void print_version (char *);
 void push (void);
 char *pwd (void);
 char *r1bindex(char *, int);
-void readconfig (struct node **, FILE *, char *, int);
+void readconfig (struct node **, FILE *, const char *, int);
 int refile (char **, char *);
 void ruserpass (char *, char **, char **);
 int remdir (char *);
diff --git a/man/inc.man b/man/inc.man
index 61f6f84..f72b1f2 100644
--- a/man/inc.man
+++ b/man/inc.man
@@ -1,4 +1,4 @@
-.TH INC %manext1% "April 18, 2014" "%nmhversion%"
+.TH INC %manext1% "November 25, 2014" "%nmhversion%"
 .\"
 .\" %nmhwarning%
 .\"
@@ -37,6 +37,8 @@ inc \- incorporate new mail
 .RB [ \-sasl " | " \-nosasl ]
 .RB [ \-saslmech
 .IR mechanism ]
+.RB [ \-oauth
+.IR service ]
 .RB [ \-snoop ]
 .RB [ \-version ]
 .RB [ \-help ]
@@ -242,7 +244,9 @@ the user's maildrop from the POP service host to the named file.
 For debugging purposes, you may give the switch
 .BR \-snoop ,
 which will allow you to watch the POP transaction take place
-between you and the POP server.
+between you and the POP server.  If
+.B \-oauth
+is used, the HTTP transaction is also shown.
 .PP
 If
 .B nmh
@@ -264,6 +268,29 @@ Encrypted traffic is labelled with `(encrypted)' and `(decrypted)'
 when viewing the POP transaction with the
 .B \-snoop
 switch.
+.PP
+If
+.B nmh
+has been compiled with OAuth support, the
+.B \-oauth
+switch will enable OAuth authentication.  The
+.B \-user
+switch must be used, and the
+.I user-name
+must be an email address the user has for that service.  Before using this,
+the user must authorize nmh by running
+.B mhlogin
+and grant authorization to that account.  Only
+.B -oauth
+.I gmail
+is supported.  See the
+.B mhlogin
+man page for more details.
+.PP
+Gmail only supports POP3 over TLS, but
+.B inc
+has no TLS support.  To work around this, use something like
+.B -proxy 'openssl s_client -connect %h:995 -CAfile /etc/ssl/certs/ca-certificates.crt -quiet'
 .SH FILES
 .PD 0
 .TP 20
@@ -302,6 +329,7 @@ To name sequences denoting unseen messages.
 .IR scan (1),
 .IR mh\-mail (5),
 .IR mh\-profile (5),
+.IR mhlogin (1),
 .IR post (8),
 .IR rcvstore (1)
 .SH DEFAULTS
diff --git a/man/mhlogin.man b/man/mhlogin.man
new file mode 100644
index 0000000..0651fdc
--- /dev/null
+++ b/man/mhlogin.man
@@ -0,0 +1,63 @@
+.\"
+.\" %nmhwarning%
+.\"
+.TH SEND %manext1% "November 25, 2014" "%nmhversion%"
+.SH NAME
+mhlogin \- login to external (OAuth) services
+.SH SYNOPSIS
+.HP 5
+.na
+.B mhlogin
+.RB \-oauth
+.IR service
+.RB [ \-snoop ]
+.RB [ \-version ]
+.RB [ \-help ]
+.ad
+.SH DESCRIPTION
+.B Mhlogin
+currently only supports OAuth for Gmail.  Run
+.B mhlogin
+.B -oauth
+.I gmail
+and load the printed URL in your browser.  Login to a Gmail account, grant
+authorization, and copy and paste the code into the
+.B mhlogin
+prompt.  Be sure to use the same account with the
+.B -user
+switch to
+.B send
+.PP
+The
+.B \-snoop
+switch can be used to view the HTTP transaction.
+.PP
+All parameters configuring the service may be overridden by profile components,
+and even though only Gmail is supported out of the box, the user can define
+new services entirely in the profile.  Profile components are prefixed by
+.I
+oauth-
+.I
+service-
+for example
+.I oauth-gmail-credential-file
+which specifies where
+.B mhlogin
+should write credentials and where
+.B send
+should read them.
+.SH "PROFILE COMPONENTS"
+.fc ^ ~
+.nf
+.ta 2.4i
+.ta \w'ExtraBigProfileName          'u
+^oauth-gmail-credential-file:~^oauth-gmail
+^oauth-gmail-client_id:~^nmh project client_id
+^oauth-gmail-client_secret:~^nmh project client_secret
+^oauth-gmail-auth_endpoint:~^https://accounts.google.com/o/oauth2/auth
+^oauth-gmail-redirect_uri:~^urn:ietf:wg:oauth:2.0:oob
+^oauth-gmail-token_endpoint:~^https://accounts.google.com/o/oauth2/token
+^oauth-gmail-scope:~^https://mail.google.com/
+.fi
+.SH "SEE ALSO"
+.IR send (1)
diff --git a/man/msgchk.man b/man/msgchk.man
index 0d5d6c9..5b02c52 100644
--- a/man/msgchk.man
+++ b/man/msgchk.man
@@ -1,4 +1,4 @@
-.TH MSGCHK %manext1% "April 14, 2013" "%nmhversion%"
+.TH MSGCHK %manext1% "November 25, 2014" "%nmhversion%"
 .\"
 .\" %nmhwarning%
 .\"
@@ -20,6 +20,8 @@ all/mail/nomail ]
 .RB [ \-sasl ]
 .RB [ \-saslmech
 .IR mechanism ]
+.RB [ \-oauth
+.IR service ]
 .RB [ \-snoop ]
 .RI [ users
 \&... ]
@@ -97,7 +99,9 @@ For debugging purposes, there is also a switch
 .BR \-snoop ,
 which will
 allow you to watch the POP transaction take place between you and the
-POP server.
+POP server.  If
+.B \-oauth
+is used, the HTTP transaction is also shown.
 .PP
 If
 .B nmh
@@ -113,13 +117,36 @@ mh-profile(5) man page).  The
 switch can be used to select a particular SASL mechanism.
 .PP
 If SASL authentication is successful,
-.B inc
+.B msgchk
 will attempt to negotiate
 a security layer for session encryption.  Encrypted traffic is labelled
 with `(encrypted)' and `(decrypted)' when viewing the POP transaction
 with the
 .B \-snoop
 switch.
+.PP
+If
+.B nmh
+has been compiled with OAuth support, the
+.B \-oauth
+switch will enable OAuth authentication.  The
+.B \-user
+switch must be used, and the
+.I user-name
+must be an email address the user has for that service.  Before using this,
+the user must authorize nmh by running
+.B mhlogin
+and grant authorization to that account.  Only
+.B -oauth
+.I gmail
+is supported.  See the
+.B mhlogin
+man page for more details.
+.PP
+Gmail only supports POP3 over TLS, but
+.B msgchk
+has no TLS support.  To work around this, use something like
+.B -proxy 'openssl s_client -connect %h:995 -CAfile /etc/ssl/certs/ca-certificates.crt -quiet'
 .SH FILES
 .fc ^ ~
 .nf
diff --git a/man/send.man b/man/send.man
index 8333367..29e6e24 100644
--- a/man/send.man
+++ b/man/send.man
@@ -1,7 +1,7 @@
 .\"
 .\" %nmhwarning%
 .\"
-.TH SEND %manext1% "July 8, 2014" "%nmhversion%"
+.TH SEND %manext1% "November 25, 2014" "%nmhversion%"
 .SH NAME
 send \- send a message
 .SH SYNOPSIS
@@ -25,6 +25,8 @@ send \- send a message
 .RB [ \-msgid " | " \-nomsgid ]
 .RB [ \-messageid
 .IR localname " | " random ]
+.RB [ \-oauth
+.IR service ]
 .RB [ \-push " | " \-nopush ]
 .RB [ \-split
 .IR seconds ]
@@ -378,7 +380,9 @@ entry).  The
 .B \-snoop
 switch can be used to view the SMTP transaction.  (Beware that the
 SMTP transaction may contain authentication information either in
-plaintext or easily decoded base64.)
+plaintext or easily decoded base64.)  If
+.B \-oauth
+is used, the HTTP transaction is also shown.
 .PP
 If
 .B nmh
@@ -416,6 +420,24 @@ underlying SASL mechanism.  A value of 0 disables encryption.
 .PP
 If
 .B nmh
+has been compiled with OAuth support, the
+.B \-oauth
+switch will enable OAuth authentication.  The
+.B \-user
+switch must be used, and the
+.I user-name
+must be an email address the user has for that service.  Before using this,
+the user must authorize nmh by running
+.B mhlogin
+and grant authorization to that account.  Only
+.B -oauth
+.I gmail
+is supported.  See the
+.B mhlogin
+man page for more details.
+.PP
+If
+.B nmh
 has been compiled with TLS support, the
 .B \-tls
 and
@@ -478,6 +500,7 @@ for more information.
 .IR forw (1),
 .IR mhbuild (1),
 .IR mhparam (1),
+.IR mhlogin (1),
 .IR repl (1),
 .IR whatnow (1),
 .IR mh\-alias (5),
diff --git a/mts/smtp/smtp.c b/mts/smtp/smtp.c
index 873e0cd..39cb713 100644
--- a/mts/smtp/smtp.c
+++ b/mts/smtp/smtp.c
@@ -76,9 +76,7 @@
 #define SM_DOT 600 /* see above */
 #define SM_QUIT 30
 #define SM_CLOS 10
-#ifdef CYRUS_SASL
 #define SM_AUTH  45
-#endif /* CYRUS_SASL */
 
 static int sm_addrs = 0;
 static int sm_alarmed = 0;
@@ -153,7 +151,7 @@ static char *EHLOkeys[MAXEHLO + 1];
  * static prototypes
  */
 static int smtp_init (char *, char *, char *, int, int, int, int, int,
-      char *, char *, int);
+      char *, char *, const char *, int);
 static int sendmail_init (char *, char *, int, int, int, int, int,
                           char *, char *);
 
@@ -173,6 +171,7 @@ static int sm_fputs(char *);
 static int sm_fputc(int);
 static void sm_fflush(void);
 static int sm_fgets(char *, int, FILE *);
+static int sm_auth_xoauth2(const char *);
 
 #ifdef CYRUS_SASL
 /*
@@ -184,11 +183,13 @@ static int sm_auth_sasl(char *, int, char *, char *);
 
 int
 sm_init (char *client, char *server, char *port, int watch, int verbose,
-         int debug, int sasl, int saslssf, char *saslmech, char *user, int tls)
+         int debug, int sasl, int saslssf, char *saslmech, char *user,
+         const char *xoauth_client_res, int tls)
 {
     if (sm_mts == MTS_SMTP)
  return smtp_init (client, server, port, watch, verbose,
-  debug, sasl, saslssf, saslmech, user, tls);
+  debug, sasl, saslssf, saslmech, user,
+                          xoauth_client_res, tls);
     else
  return sendmail_init (client, server, watch, verbose,
                               debug, sasl, saslssf, saslmech, user);
@@ -197,12 +198,11 @@ sm_init (char *client, char *server, char *port, int watch, int verbose,
 static int
 smtp_init (char *client, char *server, char *port, int watch, int verbose,
    int debug,
-           int sasl, int saslssf, char *saslmech, char *user, int tls)
+           int sasl, int saslssf, char *saslmech, char *user,
+           const char *xoauth_client_res, int tls)
 {
     int result, sd1, sd2;
-#ifdef CYRUS_SASL
-    char *server_mechs;
-#else  /* CYRUS_SASL */
+#ifndef CYRUS_SASL
     NMH_UNUSED (sasl);
     NMH_UNUSED (saslssf);
     NMH_UNUSED (saslmech);
@@ -362,6 +362,7 @@ smtp_init (char *client, char *server, char *port, int watch, int verbose,
      */
 
     if (sasl) {
+        char *server_mechs;
  if (! (server_mechs = EHLOset("AUTH"))) {
     sm_end(NOTOK);
     return sm_ierror("SMTP server does not support SASL");
@@ -382,6 +383,19 @@ smtp_init (char *client, char *server, char *port, int watch, int verbose,
     }
 #endif /* CYRUS_SASL */
 
+    if (xoauth_client_res != NULL) {
+        char *server_mechs;
+ if ((server_mechs = EHLOset("AUTH")) == NULL
+            || stringdex("XOAUTH2", server_mechs) == -1) {
+    sm_end(NOTOK);
+    return sm_ierror("SMTP server does not support SASL XOAUTH2");
+ }
+ if (sm_auth_xoauth2(xoauth_client_res) != RP_OK) {
+    sm_end(NOTOK);
+    return NOTOK;
+ }
+    }
+
 send_options: ;
     if (watch && EHLOset ("XVRB"))
  smtalk (SM_HELO, "VERB on");
@@ -1132,6 +1146,36 @@ sm_get_pass(sasl_conn_t *conn, void *context, int id,
 }
 #endif /* CYRUS_SASL */
 
+/* https://developers.google.com/gmail/xoauth2_protocol */
+static int
+sm_auth_xoauth2(const char *client_res)
+{
+    int status = smtalk(SM_AUTH, "AUTH XOAUTH2 %s", client_res);
+    if (status == 235) {
+        /* It worked! */
+        return RP_OK;
+    }
+
+    /*
+     * Status is 334 and sm_reply.text contains base64-encoded JSON.  As far as
+     * epg can tell, no matter the error, the JSON is always the same:
+     * {"status":"400","schemes":"Bearer","scope":"https://mail.google.com/"}
+     * I tried these errors:
+     * - garbage token
+     * - expired token
+     * - wrong scope
+     * - wrong username
+     */
+    /* Then we're supposed to send an empty response ("\r\n"). */
+    smtalk(SM_AUTH, "");
+    /*
+     * And now we always get this, again, no matter the error:
+     * 535-5.7.8 Username and Password not accepted. Learn more at
+     * 535 5.7.8 http://support.google.com/mail/bin/answer.py?answer=14257
+     */
+    return RP_BHST;
+}
+
 static int
 sm_ierror (char *fmt, ...)
 {
diff --git a/mts/smtp/smtp.h b/mts/smtp/smtp.h
index 72caacc..268de50 100644
--- a/mts/smtp/smtp.h
+++ b/mts/smtp/smtp.h
@@ -16,7 +16,8 @@ struct smtp {
  * prototypes
  */
 /* int client (); */
-int sm_init (char *, char *, char *, int, int, int, int, int, char *, char *, int);
+int sm_init (char *, char *, char *, int, int, int, int, int, char *, char *,
+             const char *, int);
 int sm_winit (char *);
 int sm_wadr (char *, char *, char *);
 int sm_waend (void);
diff --git a/sbr/error.c b/sbr/error.c
index 0b6d777..184c950 100644
--- a/sbr/error.c
+++ b/sbr/error.c
@@ -31,7 +31,7 @@ advise (char *what, char *fmt, ...)
  * print out error message and exit
  */
 void
-adios (char *what, char *fmt, ...)
+adios (char *what, const char *fmt, ...)
 {
     va_list ap;
 
@@ -60,7 +60,7 @@ admonish (char *what, char *fmt, ...)
  * main routine for printing error messages.
  */
 void
-advertise (char *what, char *tail, char *fmt, va_list ap)
+advertise (char *what, char *tail, const char *fmt, va_list ap)
 {
     int eindex = errno;
     char buffer[BUFSIZ], err[BUFSIZ];
diff --git a/sbr/oauth.c b/sbr/oauth.c
new file mode 100644
index 0000000..6c284d8
--- /dev/null
+++ b/sbr/oauth.c
@@ -0,0 +1,1158 @@
+/*
+ * This code is Copyright (c) 2014, by the authors of nmh.  See the
+ * COPYRIGHT file in the root directory of the nmh distribution for
+ * complete copyright information.
+ */
+
+#include <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
diff --git a/sbr/readconfig.c b/sbr/readconfig.c
index 2e319fe..5d11689 100644
--- a/sbr/readconfig.c
+++ b/sbr/readconfig.c
@@ -42,7 +42,7 @@ static struct node **opp = NULL;
 
 
 void
-readconfig (struct node **npp, FILE *ib, char *file, int ctx)
+readconfig (struct node **npp, FILE *ib, const char *file, int ctx)
 {
     register int state;
     register char *cp;
diff --git a/test/fakehttp.c b/test/fakehttp.c
new file mode 100644
index 0000000..f773a73
--- /dev/null
+++ b/test/fakehttp.c
@@ -0,0 +1,126 @@
+/*
+ * fakehttp - A fake HTTP server used by the nmh test suite
+ *
+ * This code is Copyright (c) 2014, by the authors of nmh.  See the
+ * COPYRIGHT file in the root directory of the nmh distribution for
+ * complete copyright information.
+ */
+
+#include <errno.h>
+#include <sys/stat.h>
+#include <sys/types.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+#define LINESIZE 1024
+#define PIDFN "/tmp/fakehttp.pid"
+
+int serve(const char *, const char *);
+void putcrlf(int, char *);
+
+static void
+save_req(int conn, FILE *req)
+{
+    char buf[BUFSIZ];
+    ssize_t r;
+    int e;                      /* used to save errno */
+    int started = 0;            /* whether the request has started coming in */
+
+    if (fcntl(conn, F_SETFL, O_NONBLOCK) < 0) {
+        fprintf(stderr, "Unable to make socket non-blocking: %s\n",
+                strerror(errno));
+        exit(1);
+    }
+
+    for (;;) {
+        r = read(conn, buf, sizeof buf);
+        if (!started) {
+            /* First keep trying until some data is ready; for testing, don't
+             * bother with using select to wait for input. */
+            if (r < 0) {
+                e = errno;
+                if (e == EAGAIN || e == EWOULDBLOCK) {
+                    continue;   /* keep waiting */
+                }
+                fclose(req);
+                fprintf(stderr, "Unable to read socket: %s\n", strerror(e));
+                exit(1);
+            }
+            /* Request is here.  Fall through to the fwrite below and keep
+             * reading. */
+            started = 1;
+        }
+        if (r < 0) {
+            e = errno;
+            fputs("\n", req);   /* req body usually has no newline */
+            fclose(req);
+            if (e != EAGAIN && e != EWOULDBLOCK) {
+                fprintf(stderr, "Unable to read socket: %s\n", strerror(e));
+                exit(1);
+            }
+            /* For testing, we can get away without understand the HTTP request
+             * and just treating the would-block case as meaning the request is
+             * all done. */
+            return;
+        }
+        /* make tests simpler by eliding carriage-returns? */
+        fwrite(buf, 1, r, req);
+    }
+}
+
+static void
+send_res(int conn, FILE *res)
+{
+    size_t size;
+    ssize_t len;
+    char *res_line = NULL;
+
+    while ((len = getline(&res_line, &size, res)) > 0) {
+        res_line[len - 1] = '\0';
+        putcrlf(conn, res_line);
+    }
+    free(res_line);
+    if (!feof(res)) {
+        fprintf(stderr, "read response failed: %s\n", strerror(errno));
+        exit(1);
+    }
+}
+
+int
+main(int argc, char *argv[])
+{
+    struct st;
+    int conn;
+    FILE *req, *res;
+
+    if (argc != 4) {
+        fprintf(stderr, "Usage: %s output-filename port response\n",
+                argv[0]);
+        exit(1);
+    }
+
+    if (!(req = fopen(argv[1], "w"))) {
+        fprintf(stderr, "Unable to open output file \"%s\": %s\n",
+                argv[1], strerror(errno));
+        exit(1);
+    }
+
+    if (!(res = fopen(argv[3], "r"))) {
+        fprintf(stderr, "Unable to open response \"%s\": %s\n",
+                argv[3], strerror(errno));
+        exit(1);
+    }
+
+    conn = serve(PIDFN, argv[2]);
+
+    save_req(conn, req);
+
+    send_res(conn, res);
+
+    close(conn);
+
+    return 0;
+}
diff --git a/test/fakepop.c b/test/fakepop.c
index 4d02354..bc2baae 100644
--- a/test/fakepop.c
+++ b/test/fakepop.c
@@ -10,36 +10,28 @@
 #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 *);
@@ -47,16 +39,12 @@ static char *readmessage(FILE *);
 int
 main(int argc, char *argv[])
 {
- struct addrinfo hints, *res;
- struct stat st;
- FILE **mfiles, *pid;
+ FILE **mfiles;
  char line[LINESIZE];
- fd_set readfd;
- struct timeval tv;
- pid_t child;
- int rc, l, s, on, user = 0, pass = 0, i, j;
+ int rc, s, user = 0, auth = 0, i, j;
  int numfiles;
  size_t *octets;
+ const char *xoauth;
 
  if (argc < 5) {
  fprintf(stderr, "Usage: %s port username "
@@ -64,6 +52,12 @@ main(int argc, char *argv[])
  exit(1);
  }
 
+ if (strcmp(argv[2], "XOAUTH") == 0) {
+ xoauth = argv[3];
+ } else {
+ xoauth = NULL;
+ }
+
  numfiles = argc - 4;
 
  mfiles = malloc(sizeof(FILE *) * numfiles);
@@ -105,153 +99,13 @@ main(int argc, char *argv[])
  rewind(mfiles[j]);
  }
 
- /*
- * If there is a pid file around, kill the previously running
- * fakepop process.
- */
-
- if (stat(PIDFILE, &st) == 0) {
- long oldpid;
-
- if (!(pid = fopen(PIDFILE, "r"))) {
- fprintf(stderr, "Cannot open " PIDFILE
- " (%s), continuing ...\n", strerror(errno));
- } else {
- rc = fscanf(pid, "%ld", &oldpid);
- fclose(pid);
-
- if (rc != 1) {
- fprintf(stderr, "Unable to parse pid in "
- PIDFILE ", continuing ...\n");
- } else {
- kill((pid_t) oldpid, SIGTERM);
- }
- }
-
- unlink(PIDFILE);
- }
-
- memset(&hints, 0, sizeof(hints));
-
- hints.ai_family = PF_INET;
- hints.ai_socktype = SOCK_STREAM;
- hints.ai_protocol = IPPROTO_TCP;
- hints.ai_flags = AI_PASSIVE;
-
- rc = getaddrinfo("127.0.0.1", argv[1], &hints, &res);
-
- if (rc) {
- fprintf(stderr, "Unable to resolve localhost/%s: %s\n",
- argv[1], gai_strerror(rc));
- exit(1);
- }
-
- l = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
-
- if (l == -1) {
- fprintf(stderr, "Unable to create listening socket: %s\n",
- strerror(errno));
- exit(1);
- }
-
- on = 1;
-
- if (setsockopt(l, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1) {
- fprintf(stderr, "Unable to set SO_REUSEADDR: %s\n",
- strerror(errno));
- exit(1);
- }
-
- if (bind(l, res->ai_addr, res->ai_addrlen) == -1) {
- fprintf(stderr, "Unable to bind socket: %s\n", strerror(errno));
- exit(1);
- }
-
- if (listen(l, 1) == -1) {
- fprintf(stderr, "Unable to listen on socket: %s\n",
- strerror(errno));
- exit(1);
- }
-
- /*
- * Fork off a copy of ourselves, print out our child pid, then
- * exit.
- */
-
- switch (child = fork()) {
- case -1:
- fprintf(stderr, "Unable to fork child: %s\n", strerror(errno));
- exit(1);
- break;
- case 0:
- /*
- * Close stdin and stdout so $() in the shell will get an
- * EOF.  For now leave stderr open.
- */
- fclose(stdin);
- fclose(stdout);
- break;
- default:
- printf("%ld\n", (long) child);
- exit(0);
- }
-
- /*
- * Now that our socket and files are set up, wait 30 seconds for
- * a connection.  If there isn't one, then exit.
- */
-
- if (!(pid = fopen(PIDFILE, "w"))) {
- fprintf(stderr, "Cannot open " PIDFILE ": %s\n",
- strerror(errno));
- exit(1);
- }
-
- fprintf(pid, "%ld\n", (long) getpid());
- fclose(pid);
-
- signal(SIGTERM, handleterm);
- atexit(killpidfile);
-
- FD_ZERO(&readfd);
- FD_SET(l, &readfd);
-
- tv.tv_sec = 30;
- tv.tv_usec = 0;
-
- rc = select(l + 1, &readfd, NULL, NULL, &tv);
-
- if (rc < 0) {
- fprintf(stderr, "select() failed: %s\n", strerror(errno));
- exit(1);
- }
-
- /*
- * If we get a timeout, just silently exit
- */
-
- if (rc == 0) {
- exit(1);
- }
-
- /*
- * We got a connection; accept it.  Right after that close our
- * listening socket so we won't get any more connections on it.
- */
-
- if ((s = accept(l, NULL, NULL)) == -1) {
- fprintf(stderr, "Unable to accept connection: %s\n",
- strerror(errno));
- exit(1);
- }
-
- close(l);
+ s = serve(PIDFILE, argv[1]);
 
  /*
  * Pretend to be a POP server
  */
 
- putpop(s, "+OK Not really a POP server, but we play one on TV");
+ putcrlf(s, "+OK Not really a POP server, but we play one on TV");
 
  for (;;) {
  char linebuf[LINESIZE];
@@ -263,26 +117,42 @@ main(int argc, char *argv[])
 
  if (strcasecmp(linebuf, "CAPA") == 0) {
  putpopbulk(s, "+OK We have no capabilities, really\r\n"
-   "FAKE-CAPABILITY\r\n.\r\n");
+   "FAKE-CAPABILITY\r\n");
+ if (xoauth != NULL) {
+ putcrlf(s, "SASL XOAUTH2");
+ }
+ putcrlf(s, ".");
  } else if (strncasecmp(linebuf, "USER ", 5) == 0) {
  if (strcmp(linebuf + 5, argv[2]) == 0) {
- putpop(s, "+OK Niiiice!");
+ putcrlf(s, "+OK Niiiice!");
  user = 1;
  } else {
- putpop(s, "-ERR Don't play me, bro!");
+ putcrlf(s, "-ERR Don't play me, bro!");
  }
  } else if (strncasecmp(linebuf, "PASS ", 5) == 0) {
  CHECKUSER();
  if (strcmp(linebuf + 5, argv[3]) == 0) {
- putpop(s, "+OK Aren't you a sight "
+ putcrlf(s, "+OK Aren't you a sight "
        "for sore eyes!");
- pass = 1;
+ auth = 1;
  } else {
- putpop(s, "-ERR C'mon!");
+ putcrlf(s, "-ERR C'mon!");
  }
+ } else if (xoauth != NULL
+   && strncasecmp(linebuf, "AUTH XOAUTH2", 12) == 0) {
+ if (strstr(linebuf, xoauth) == NULL) {
+ putcrlf(s, "+ base64-json-err");
+ rc = getpop(s, linebuf, sizeof(linebuf));
+ if (rc != 0)
+ break; /* Error or EOF */
+ putcrlf(s, "-ERR [AUTH] Invalid credentials.");
+ continue;
+ }
+ putcrlf(s, "+OK Welcome.");
+ auth = 1;
  } else if (strcasecmp(linebuf, "STAT") == 0) {
  size_t total = 0;
- CHECKUSERPASS();
+ CHECKAUTH();
  for (i = 0, j = 0; i < numfiles; i++) {
  if (mfiles[i]) {
  total += octets[i];
@@ -291,53 +161,53 @@ main(int argc, char *argv[])
  }
  snprintf(linebuf, sizeof(linebuf),
  "+OK %d %d", i, (int) total);
- putpop(s, linebuf);
+ putcrlf(s, linebuf);
  } else if (strncasecmp(linebuf, "RETR ", 5) == 0) {
- CHECKUSERPASS();
+ CHECKAUTH();
  rc = sscanf(linebuf + 5, "%d", &i);
  if (rc != 1) {
- putpop(s, "-ERR Whaaaa...?");
+ putcrlf(s, "-ERR Whaaaa...?");
  continue;
  }
  if (i < 1 || i > numfiles) {
- putpop(s, "-ERR That message number is "
+ putcrlf(s, "-ERR That message number is "
        "out of range, jerkface!");
  continue;
  }
  if (mfiles[i - 1] == NULL) {
- putpop(s, "-ERR Sorry, don't have it anymore");
+ putcrlf(s, "-ERR Sorry, don't have it anymore");
  } else {
  char *buf = readmessage(mfiles[i - 1]);
- putpop(s, "+OK Here you go ...");
+ putcrlf(s, "+OK Here you go ...");
  putpopbulk(s, buf);
  free(buf);
  }
  } else if (strncasecmp(linebuf, "DELE ", 5) == 0) {
- CHECKUSERPASS();
+ CHECKAUTH();
  rc = sscanf(linebuf + 5, "%d", &i);
  if (rc != 1) {
- putpop(s, "-ERR Whaaaa...?");
+ putcrlf(s, "-ERR Whaaaa...?");
  continue;
  }
  if (i < 1 || i > numfiles) {
- putpop(s, "-ERR That message number is "
+ putcrlf(s, "-ERR That message number is "
        "out of range, jerkface!");
  continue;
  }
  if (mfiles[i - 1] == NULL) {
- putpop(s, "-ERR Um, didn't you tell me "
+ putcrlf(s, "-ERR Um, didn't you tell me "
        "to delete it already?");
  } else {
  fclose(mfiles[i - 1]);
  mfiles[i - 1] = NULL;
- putpop(s, "+OK Alright man, I got rid of it");
+ putcrlf(s, "+OK Alright man, I got rid of it");
  }
  } else if (strcasecmp(linebuf, "QUIT") == 0) {
- putpop(s, "+OK See ya, wouldn't want to be ya!");
+ putcrlf(s, "+OK See ya, wouldn't want to be ya!");
  close(s);
  break;
  } else {
- putpop(s, "-ERR Um, what?");
+ putcrlf(s, "-ERR Um, what?");
  }
  }
 
@@ -345,25 +215,6 @@ main(int argc, char *argv[])
 }
 
 /*
- * Send one line to the POP client
- */
-
-static void
-putpop(int socket, char *data)
-{
- struct iovec iov[2];
-
- iov[0].iov_base = data;
- iov[0].iov_len = strlen(data);
- iov[1].iov_base = "\r\n";
- iov[1].iov_len = 2;
-
- if (writev(socket, iov, 2) < 0) {
-    perror ("writev");
- }
-}
-
-/*
  * Put one big buffer to the POP server.  Should have already had the line
  * endings set up and dot-stuffed if necessary.
  */
@@ -466,27 +317,3 @@ readmessage(FILE *file)
 
  return buffer;
 }
-
-/*
- * Handle a SIGTERM
- */
-
-static void
-handleterm(int signal)
-{
- (void) signal;
-
- killpidfile();
- fflush(NULL);
- _exit(1);
-}
-
-/*
- * Get rid of our pid file
- */
-
-static void
-killpidfile(void)
-{
- unlink(PIDFILE);
-}
diff --git a/test/fakesmtp.c b/test/fakesmtp.c
index b812f23..42d4f18 100644
--- a/test/fakesmtp.c
+++ b/test/fakesmtp.c
@@ -10,35 +10,37 @@
 #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]);
@@ -51,156 +53,14 @@ main(int argc, char *argv[])
  exit(1);
  }
 
- /*
- * If there is a pid file already around, kill the previously running
- * fakesmtp process.  Hopefully this will reduce the race conditions
- * that crop up when running the test suite.
- */
+ conn = serve(PIDFILE, argv[2]);
 
- 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);
- }
-
- close(l);
-
  /*
  * Pretend to be an SMTP server.
  */
 
- putsmtp(conn, "220 Not really an ESMTP server");
- datamode = 0;
+ putcrlf(conn, "220 Not really an ESMTP server");
+ smtp_state = SMTP_TOP;
 
  for (;;) {
  char line[LINESIZE];
@@ -212,17 +72,17 @@ main(int argc, char *argv[])
 
  fprintf(f, "%s\n", line);
 
- /*
- * If we're in DATA mode, then check to see if we've got
- * a "."; otherwise, continue
- */
-
- if (datamode) {
+ switch (smtp_state) {
+ case SMTP_DATA:
  if (strcmp(line, ".") == 0) {
- datamode = 0;
- putsmtp(conn, "250 Thanks for the info!");
+ smtp_state = SMTP_TOP;
+ putcrlf(conn, "250 Thanks for the info!");
  }
  continue;
+ case SMTP_XOAUTH_ERR:
+ smtp_state = SMTP_TOP;
+ putcrlf(conn, "535 Not no way, not no how!");
+ continue;
  }
 
  /*
@@ -232,15 +92,34 @@ main(int argc, char *argv[])
  if (strcmp(line, "QUIT") == 0) {
  fclose(f);
  f = NULL;
- putsmtp(conn, "221 Later alligator!");
+ putcrlf(conn, "221 Later alligator!");
  close(conn);
  break;
- } else if (strcmp(line, "DATA") == 0) {
- putsmtp(conn, "354 Go ahead");
- datamode = 1;
- } else {
- putsmtp(conn, "250 I'll buy that for a dollar!");
  }
+ if (strcmp(line, "DATA") == 0) {
+ putcrlf(conn, "354 Go ahead");
+ smtp_state = SMTP_DATA;
+ continue;
+ }
+ if (xoauth != NULL) {
+ /* XOAUTH2 support enabled; handle EHLO and AUTH. */
+ if (strncmp(line, "EHLO", 4) == 0) {
+ putcrlf(conn, "250-ready");
+ putcrlf(conn, "250 AUTH XOAUTH2");
+ continue;
+ }
+ if (strncmp(line, "AUTH", 4) == 0) {
+ if (strncmp(line, "AUTH XOAUTH2", 12) == 0
+    && strstr(line, xoauth) != NULL) {
+ putcrlf(conn, "235 OK come in");
+ continue;
+ }
+ putcrlf(conn, "334 base64-json-err");
+ smtp_state = SMTP_XOAUTH_ERR;
+ continue;
+ }
+ }
+ putcrlf(conn, "250 I'll buy that for a dollar!");
  }
 
  if (f)
@@ -250,25 +129,6 @@ main(int argc, char *argv[])
 }
 
 /*
- * Write a line to the SMTP client on the other end
- */
-
-static void
-putsmtp(int socket, char *data)
-{
- struct iovec iov[2];
-
- iov[0].iov_base = data;
- iov[0].iov_len = strlen(data);
- iov[1].iov_base = "\r\n";
- iov[1].iov_len = 2;
-
- if (writev(socket, iov, 2) < 0) {
-    perror ("writev");
- }
-}
-
-/*
  * Read a line (up to the \r\n)
  */
 
@@ -322,27 +182,3 @@ getsmtp(int socket, char *data)
  bytesinbuf += cc;
  }
 }
-
-/*
- * Handle a SIGTERM
- */
-
-static void
-handleterm(int signal)
-{
- (void) signal;
-
- killpidfile();
- fflush(NULL);
- _exit(1);
-}
-
-/*
- * Get rid of our pid file
- */
-
-static void
-killpidfile(void)
-{
- unlink(PIDFILE);
-}
diff --git a/test/oauth/common.sh b/test/oauth/common.sh
new file mode 100644
index 0000000..06ab14c
--- /dev/null
+++ b/test/oauth/common.sh
@@ -0,0 +1,173 @@
+# Common routines for OAuth tests
+
+. "${MH_OBJ_DIR}/test/common.sh"
+
+setup_test
+
+if test "${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: <a href="http://127.0.0.1:$">http://127.0.0.1:${http_port}/oauth/auth
+oauth-test-token_endpoint: <a href="http://127.0.0.1:$">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 <[hidden email]>
+To: Some Other User <[hidden email]>
+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 <[hidden email]>
+To: Somebody Else <[hidden email]>
+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 [hidden email] -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 [hidden email]" "$@"
+}
+
+test_send_only_fakesmtp() {
+    start_fakesmtp
+    test_send_no_servers "$@"
+}
+
+test_send() {
+    start_fakehttp
+    test_send_only_fakesmtp "$@"
+    check "${testname}.http-req" "${testname}.expected-http-req"
+}
+
+check_http_req() {
+    check "${testname}.http-req" "${testname}.expected-http-req"
+}
+
+check_creds_private() {
+    f="${MHTMPDIR}/oauth-test"
+    if ls -dl "$f" | grep '^-rw-------' > /dev/null 2>&1; then
+        :
+    else
+        echo "$f permissions not private"
+        failed=`expr ${failed:-0} + 1`
+    fi
+}
+
+check_creds() {
+    # It's hard to calculate the exact expiration time mhlogin is going to use,
+    # so we'll just use sed to remove the actual time so we can easily compare
+    # it against our "correct" output.
+    f="${MHTMPDIR}/oauth-test"
+
+    sed 's/^expire:.*/expire:/' "$f" > "$f".notime
+    check "$f".notime "${testname}.expected-creds"
+    rm "$f"
+}
diff --git a/test/oauth/test-inc b/test/oauth/test-inc
new file mode 100755
index 0000000..8c71e62
--- /dev/null
+++ b/test/oauth/test-inc
@@ -0,0 +1,116 @@
+#!/bin/sh
+#
+# Test the XOAUTH2 support in inc
+#
+
+if test -z "${MH_OBJ_DIR}"; then
+    srcdir=`dirname "$0"`/../..
+    MH_OBJ_DIR=`cd "${srcdir}" && pwd`; export MH_OBJ_DIR
+fi
+
+. "${srcdir}/test/oauth/common.sh"
+
+setup_pop
+
+#
+# success cases
+#
+
+# TEST
+echo 'access token ready, pop server accepts message'
+
+fake_creds <<EOF
+access: test-access
+expire: 2000000000
+EOF
+
+start_pop_xoauth
+
+test_inc_success
+
+# TEST
+echo 'expired access token, refresh works, pop server accepts message'
+
+fake_creds <<EOF
+access: old-access
+refresh: test-refresh
+expire: 1414303986
+EOF
+
+expect_http_post_refresh
+
+fake_json_response <<EOF
+{
+  "access_token": "test-access",
+  "token_type": "Bearer",
+  "expires_in": 3600
+}
+EOF
+
+expect_creds <<EOF
+access: test-access
+refresh: test-refresh
+expire:
+EOF
+
+start_fakehttp
+start_pop_xoauth
+
+test_inc_success
+
+check_http_req
+
+#
+# fail cases
+#
+
+# TEST
+echo 'refresh gets proper error from http'
+
+fake_creds <<EOF
+access: test-access
+refresh: test-refresh
+EOF
+
+expect_http_post_refresh
+
+fake_http_response '400 Bad Request' <<EOF
+Content-Type: application/json
+
+{
+  "error": "invalid_request"
+}
+EOF
+
+start_fakehttp
+
+test_inc 'inc: error refreshing OAuth2 token
+inc: bad OAuth request; re-run with -snoop and send REDACTED output to nmh-workers'
+
+check_http_req
+
+# TEST
+echo 'pop server rejects token'
+
+fake_creds <<EOF
+access: wrong-access
+expire: 2000000000
+EOF
+
+start_pop_xoauth
+
+test_inc 'inc: -ERR [AUTH] Invalid credentials.'
+
+# TEST
+echo "pop server doesn't support oauth"
+
+fake_creds <<EOF
+access: test-access
+expire: 2000000000
+EOF
+
+start_pop testuser testpass
+
+test_inc 'inc: POP server does not support SASL'
+
+exit ${failed:-0}
diff --git a/test/oauth/test-mhlogin b/test/oauth/test-mhlogin
new file mode 100755
index 0000000..6103f70
--- /dev/null
+++ b/test/oauth/test-mhlogin
@@ -0,0 +1,257 @@
+#!/bin/sh
+#
+# Test mhlogin
+#
+
+if test -z "${MH_OBJ_DIR}"; then
+    srcdir=`dirname "$0"`/../..
+    MH_OBJ_DIR=`cd "${srcdir}" && pwd`; export MH_OBJ_DIR
+fi
+
+. "${srcdir}/test/oauth/common.sh"
+
+expect_no_creds() {
+    cat /dev/null > "${testname}.expected-creds"
+    cat /dev/null > "${MHTMPDIR}/oauth-test"
+    chmod 600 "${MHTMPDIR}/oauth-test"
+}
+
+test_mhlogin() {
+    start_fakehttp
+    run_test 'eval echo code | mhlogin -oauth test' \
+"Load the following URL in your browser and authorize nmh to access test:
+
+<a href="http://127.0.0.1:$">http://127.0.0.1:${http_port}/oauth/auth?response_type=code&client_id=test-id&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&scope=test-scope
+
+Enter the authorization code: $1"
+    check_http_req
+    check_creds_private
+    check_creds
+}
+
+test_mhlogin_invalid_response() {
+    test_mhlogin 'mhlogin: error exchanging code for OAuth2 token
+mhlogin: invalid response'
+}
+
+#
+# success cases
+#
+
+# TEST
+echo 'mhlogin receives access and expiration'
+
+expect_http_post_code
+
+fake_json_response <<EOF
+{
+  "access_token": "test-access",
+  "token_type": "Bearer",
+  "expires_in": 3600
+}
+EOF
+
+expect_creds <<EOF
+access: test-access
+expire:
+EOF
+
+test_mhlogin
+
+# TEST
+echo 'mhlogin receives access and refresh'
+
+expect_http_post_code
+
+fake_json_response <<EOF
+{
+  "access_token": "test-access",
+  "token_type": "Bearer"
+}
+EOF
+
+expect_creds <<EOF
+access: test-access
+EOF
+
+test_mhlogin
+
+# TEST
+echo 'mhlogin receives access, expiration, and refresh'
+
+expect_http_post_code
+
+fake_json_response <<EOF
+{
+  "access_token": "test-access",
+  "refresh_token": "refresh-token",
+  "expires_in": 3600,
+  "token_type": "Bearer"
+}
+EOF
+
+expect_creds <<EOF
+access: test-access
+refresh: refresh-token
+expire:
+EOF
+
+test_mhlogin
+
+# TEST
+echo 'mhlogin receives refresh only'
+
+expect_http_post_code
+
+fake_json_response <<EOF
+{
+  "refresh_token": "refresh-token",
+  "token_type": "Bearer"
+}
+EOF
+
+expect_creds <<EOF
+refresh: refresh-token
+EOF
+
+test_mhlogin
+
+# TEST
+echo 'mhlogin receives token_type only'
+
+expect_http_post_code
+
+fake_json_response <<EOF
+{
+  "token_type": "Bearer"
+}
+EOF
+
+expect_no_creds
+
+test_mhlogin_invalid_response
+
+# TEST
+echo 'mhlogin ignores extra bits in successful response JSON'
+
+expect_http_post_code
+
+fake_json_response <<EOF
+{
+  "access_token": "test-access",
+  "refresh_token": "refresh-token",
+  "extra_object": {
+    "a": 1,
+    "b": [1, 2, 3],
+    "c": [{}, {"foo": "bar"}]
+  },
+  "extra_int": 1,
+  "expires_in": 3600,
+  "token_type": "Bearer"
+}
+EOF
+
+expect_creds <<EOF
+access: test-access
+refresh: refresh-token
+expire:
+EOF
+
+test_mhlogin
+
+# TEST
+echo 'mhlogin user enters bad code'
+
+expect_http_post_code
+
+fake_http_response '400 Bad Request' <<EOF
+Content-Type: application/json
+
+{
+  "error": "invalid_grant"
+}
+EOF
+
+expect_no_creds
+
+test_mhlogin 'Code rejected; try again? '
+
+#
+# fail cases
+#
+
+# TEST
+echo 'mhlogin response has no content-type'
+
+expect_http_post_code
+
+fake_http_response '200 OK' <<EOF
+
+{
+  "access_token": "test-access",
+  "token_type": "Bearer",
+  "expires_in": 3600
+}
+EOF
+
+expect_no_creds
+
+test_mhlogin_invalid_response
+
+# TEST
+echo 'mhlogin JSON array'
+
+expect_http_post_code
+
+fake_json_response <<EOF
+[]
+EOF
+
+expect_no_creds
+
+test_mhlogin_invalid_response
+
+# TEST
+echo 'mhlogin JSON empty object'
+
+expect_http_post_code
+
+fake_json_response <<EOF
+{}
+EOF
+
+expect_no_creds
+
+test_mhlogin_invalid_response
+
+# TEST
+echo 'mhlogin empty response body'
+
+expect_http_post_code
+
+fake_json_response <<EOF
+EOF
+
+expect_no_creds
+
+test_mhlogin_invalid_response
+
+# TEST
+echo 'mhlogin gets proper error from http'
+
+expect_http_post_code
+
+fake_http_response '400 Bad Request' <<EOF
+Content-Type: application/json
+
+{
+  "error": "invalid_request"
+}
+EOF
+
+expect_no_creds
+
+test_mhlogin 'mhlogin: error exchanging code for OAuth2 token
+mhlogin: bad OAuth request; re-run with -snoop and send REDACTED output to nmh-workers'
+
+exit ${failed:-0}
diff --git a/test/oauth/test-send b/test/oauth/test-send
new file mode 100755
index 0000000..24acd9c
--- /dev/null
+++ b/test/oauth/test-send
@@ -0,0 +1,357 @@
+#!/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 [hidden email]"
+
+# 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 [hidden email]" '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:
+EOF
+
+run_prog send -draft -server 127.0.0.1 -port ${smtp_port} \
+  -oauth test -user [hidden email] > "${testname}.send-output" 2>&1 || true
+# Clear out an error message we get from libcurl on some systems (seen on
+# Ubuntu 14.04 but not FreeBSD 10).
+f="${testname}.send-output"
+sed "s/Failed to connect to 127.0.0.1 port ${http_port}: Connection refused\$//" "$f" > "$f".clean
+check "$f".clean "${testname}.expected-send-output"
+rm "$f"
+
+# TEST
+echo 'refresh gets bogus 200 response from http server'
+
+expect_http_post_refresh
+
+fake_http_response '200 OK' <<EOF
+Content-Type: text/html
+
+<html>doh!</htmxl>
+EOF
+
+test_send_only_fakehttp 'send: error refreshing OAuth2 token
+send: invalid response'
+
+# TEST
+echo 'refresh gets 500 response from http server'
+
+expect_http_post_refresh
+
+fake_http_response '500 Server Error' <<EOF
+Content-Type: text/html
+
+<html>doh!</html>
+EOF
+
+test_send_only_fakehttp 'send: error refreshing OAuth2 token
+send: invalid response'
+
+# TEST
+echo 'refresh gets proper error from http'
+
+expect_http_post_refresh
+
+fake_http_response '400 Bad Request' <<EOF
+Content-Type: application/json
+
+{
+  "error": "invalid_grant"
+}
+EOF
+
+test_send_only_fakehttp 'send: credentials rejected -- run mhlogin -oath test'
+
+# TEST
+echo 'refresh gets response too big'
+
+fake_creds <<EOF
+refresh: test-refresh
+EOF
+
+expect_http_post_refresh
+
+fake_json_response <<EOF
+{
+  "access_token": "test-access",
+  "token_type": "Bearer",
+  "expires_in": 3600
+}
+EOF
+
+awk 'BEGIN { for (i = 0; i < 8192; i++) { print "." } }' \
+    >> "${testname}.http-res"
+
+test_send_only_fakehttp 'send: error refreshing OAuth2 token
+send: refusing to process response body larger than 8192 bytes'
+
+# TEST
+echo 'smtp server rejects token'
+
+XOAUTH='not-that-one'
+
+fake_creds <<EOF
+access: test-access
+expire: 2000000000
+EOF
+
+test_send_only_fakesmtp 'post: problem initializing server; [BHST] Not no way, not no how!
+send: message not delivered to anyone'
+
+# TEST
+echo "smtp server doesn't support oauth"
+
+unset XOAUTH
+
+test_send_only_fakesmtp 'post: problem initializing server; [BHST] SMTP server does not support SASL XOAUTH2
+send: message not delivered to anyone'
+
+exit ${failed:-0}
diff --git a/test/oauth/test-share b/test/oauth/test-share
new file mode 100755
index 0000000..af88756
--- /dev/null
+++ b/test/oauth/test-share
@@ -0,0 +1,139 @@
+#!/bin/sh
+#
+# Test that inc, msgchck, and send share tokens.
+#
+
+if test -z "${MH_OBJ_DIR}"; then
+    srcdir=`dirname "$0"`/../..
+    MH_OBJ_DIR=`cd "${srcdir}" && pwd`; export MH_OBJ_DIR
+fi
+
+. "${srcdir}/test/oauth/common.sh"
+
+setup_pop
+
+export XOAUTH
+XOAUTH='dXNlcj1ub2JvZHlAZXhhbXBsZS5jb20BYXV0aD1CZWFyZXIgdGVzdC1hY2Nlc3MBAQ=='
+
+# TEST
+echo 'mhlogin then all run with no refresh'
+
+expect_http_post_code
+
+fake_json_response <<EOF
+{
+  "access_token": "test-access",
+  "token_type": "Bearer",
+  "expires_in": 3600
+}
+EOF
+
+expect_creds <<EOF
+access: test-access
+expire:
+EOF
+
+start_fakehttp
+run_test 'eval echo code | mhlogin -oauth test' \
+"Load the following URL in your browser and authorize nmh to access test:
+
+<a href="http://127.0.0.1:$">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 [hidden email]" '[hidden email] 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 [hidden email]" '[hidden email] 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 [hidden email]" '[hidden email] 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 [hidden email]" '[hidden email] has 1 message (178 bytes) on 127.0.0.1'
+
+start_pop_xoauth
+test_inc_success
+
+exit ${failed:-0}
diff --git a/test/server.c b/test/server.c
new file mode 100644
index 0000000..13f3c87
--- /dev/null
+++ b/test/server.c
@@ -0,0 +1,248 @@
+/*
+ * server.c - Utilities for fake servers used by the nmh test suite
+ *
+ * This code is Copyright (c) 2014, by the authors of nmh.  See the
+ * COPYRIGHT file in the root directory of the nmh distribution for
+ * complete copyright information.
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <netdb.h>
+#include <errno.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+#include <sys/types.h>
+#include <sys/select.h>
+#include <sys/stat.h>
+#include <sys/uio.h>
+#include <signal.h>
+
+static const char *PIDFN = NULL;
+
+static void killpidfile(void);
+static void handleterm(int);
+
+static int
+try_bind(int socket, const struct sockaddr *address, socklen_t len)
+{
+ int i, status;
+ for (i = 0; i < 5; i++) {
+ if ((status = bind(socket, address, len)) == 0) {
+ return 0;
+ }
+ sleep(1);
+ }
+
+ return status;
+}
+
+int
+serve(const char *pidfn, const char *port)
+{
+ struct addrinfo hints, *res;
+ int rc, l, conn, on;
+ FILE *pid;
+ pid_t child;
+ fd_set readfd;
+ struct stat st;
+ struct timeval tv;
+
+ PIDFN = pidfn;
+
+ /*
+ * If there is a pid file already around, kill the previously running
+ * fakesmtp process.  Hopefully this will reduce the race conditions
+ * that crop up when running the test suite.
+ */
+
+ if (stat(pidfn, &st) == 0) {
+ long oldpid;
+
+ if (!(pid = fopen(pidfn, "r"))) {
+ fprintf(stderr, "Cannot open %s (%s), continuing ...\n",
+ pidfn, strerror(errno));
+ } else {
+ rc = fscanf(pid, "%ld", &oldpid);
+ fclose(pid);
+
+ if (rc != 1) {
+ fprintf(stderr, "Unable to parse pid in %s,"
+ " continuing ...\n",
+ pidfn);
+ } else {
+ kill((pid_t) oldpid, SIGTERM);
+ }
+ }
+
+ unlink(pidfn);
+ }
+
+ memset(&hints, 0, sizeof(hints));
+
+ hints.ai_family = PF_INET;
+ hints.ai_socktype = SOCK_STREAM;
+ hints.ai_protocol = IPPROTO_TCP;
+ hints.ai_flags = AI_PASSIVE;
+
+ rc = getaddrinfo("127.0.0.1", port, &hints, &res);
+
+ if (rc) {
+ fprintf(stderr, "Unable to resolve localhost/%s: %s\n",
+ port, gai_strerror(rc));
+ exit(1);
+ }
+
+ l = socket(res->ai_family, res->ai_socktype, res->ai_protocol);
+
+ if (l == -1) {
+ fprintf(stderr, "Unable to create listening socket: %s\n",
+ strerror(errno));
+ exit(1);
+ }
+
+ on = 1;
+
+ if (setsockopt(l, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on)) == -1) {
+ fprintf(stderr, "Unable to set SO_REUSEADDR: %s\n",
+ strerror(errno));
+ exit(1);
+ }
+
+ if (try_bind(l, res->ai_addr, res->ai_addrlen) == -1) {
+ fprintf(stderr, "Unable to bind socket: %s\n", strerror(errno));
+ exit(1);
+ }
+
+ if (listen(l, 1) == -1) {
+ fprintf(stderr, "Unable to listen on socket: %s\n",
+ strerror(errno));
+ exit(1);
+ }
+
+ /*
+ * Now we fork() and print out the process ID of our child
+ * for scripts to use.  Once we do that, then exit.
+ */
+
+ child = fork();
+
+ switch (child) {
+ case -1:
+ fprintf(stderr, "Unable to fork child: %s\n", strerror(errno));
+ exit(1);
+ break;
+ case 0:
+ /*
+ * Close stdin & stdout, otherwise people can
+ * think we're still doing stuff.  For now leave stderr
+ * open.
+ */
+ fclose(stdin);
+ fclose(stdout);
+ break;
+ default:
+ /* XXX why?  it's never used... */
+ printf("%ld\n", (long) child);
+ exit(0);
+ }
+
+ /*
+ * Now that our socket & files are set up, wait 30 seconds for
+ * a connection.  If there isn't one, then exit.
+ */
+
+ if (!(pid = fopen(pidfn, "w"))) {
+ fprintf(stderr, "Cannot open %s: %s\n",
+ pidfn, strerror(errno));
+ exit(1);
+ }
+
+ fprintf(pid, "%ld\n", (long) getpid());
+ fclose(pid);
+
+ signal(SIGTERM, handleterm);
+ atexit(killpidfile);
+
+ FD_ZERO(&readfd);
+ FD_SET(l, &readfd);
+ tv.tv_sec = 30;
+ tv.tv_usec = 0;
+
+ rc = select(l + 1, &readfd, NULL, NULL, &tv);
+
+ if (rc < 0) {
+ fprintf(stderr, "select() failed: %s\n", strerror(errno));
+ exit(1);
+ }
+
+ /*
+ * I think if we get a timeout, we should just exit quietly.
+ */
+
+ if (rc == 0) {
+ exit(1);
+ }
+
+ /*
+ * Alright, got a connection!  Accept it.
+ */
+
+ if ((conn = accept(l, NULL, NULL)) == -1) {
+ fprintf(stderr, "Unable to accept connection: %s\n",
+ strerror(errno));
+ exit(1);
+ }
+
+ close(l);
+
+    return conn;
+}
+
+/*
+ * Write a line (adding \r\n) to the client on the other end
+ */
+void
+putcrlf(int socket, char *data)
+{
+ struct iovec iov[2];
+
+ iov[0].iov_base = data;
+ iov[0].iov_len = strlen(data);
+ iov[1].iov_base = "\r\n";
+ iov[1].iov_len = 2;
+
+ /* ECONNRESET just means the client already closed its end */
+ /* XXX is it useful to log errors here at all? */
+ if (writev(socket, iov, 2) < 0 && errno != ECONNRESET) {
+    perror ("writev");
+ }
+}
+
+/*
+ * Handle a SIGTERM
+ */
+
+static void
+handleterm(int signal)
+{
+ (void) signal;
+
+ killpidfile();
+ fflush(NULL);
+ _exit(1);
+}
+
+/*
+ * Get rid of our pid file
+ */
+
+static void
+killpidfile(void)
+{
+ if (PIDFN != NULL) {
+ unlink(PIDFN);
+ }
+}
diff --git a/uip/inc.c b/uip/inc.c
index 8b9fe7a..a4aea3e 100644
--- a/uip/inc.c
+++ b/uip/inc.c
@@ -52,6 +52,7 @@
     X("form formatfile", 0, FORMSW) \
     X("format string", 5, FMTSW) \
     X("host hostname", 0, HOSTSW) \
+    X("oauth service", 0, OAUTHSW) \
     X("user username", 0, USERSW) \
     X("pack file", 0, PACKSW) \
     X("nopack", 0, NPACKSW) \
@@ -185,10 +186,10 @@ main (int argc, char **argv)
     FILE *aud = NULL;
     char b[PATH_MAX + 1];
     char *maildir_copy = NULL; /* copy of mail directory because the static gets overwritten */
+    const char *oauth_svc = NULL;
 
     int nmsgs, nbytes;
     char *MAILHOST_env_variable;
-
     done=inc_done;
 
 /* absolutely the first thing we do is save our privileges,
@@ -313,6 +314,16 @@ main (int argc, char **argv)
     adios (NULL, "missing argument to %s", argp[-2]);
  continue;
 
+            case OAUTHSW:
+#ifdef OAUTH_SUPPORT
+                if (!(cp = *argp++) || *cp == '-')
+                    adios (NULL, "missing argument to %s", argp[-2]);
+                oauth_svc = cp;
+#else
+                adios (NULL, "not built with OAuth support");
+#endif
+                continue;
+
     case USERSW:
  if (!(user = *argp++) || *user == '-')
     adios (NULL, "missing argument to %s", argp[-2]);
@@ -383,12 +394,20 @@ main (int argc, char **argv)
     if (inc_type == INC_POP) {
  struct nmh_creds creds = { 0, 0, 0 };
 
+ if (oauth_svc == NULL) {
+    nmh_get_credentials (host, user, sasl, &creds);
+ } else {
+    if (user == NULL) {
+ adios (NULL, "must specify -user with -oauth");
+    }
+    creds.user = user;
+ }
+
  /*
  * initialize POP connection
  */
- nmh_get_credentials (host, user, sasl, &creds);
  if (pop_init (host, port, creds.user, creds.password, proxy, snoop,
-      sasl, saslmech) == NOTOK)
+      sasl, saslmech, oauth_svc) == NOTOK)
     adios (NULL, "%s", response);
 
  /* Check if there are any messages */
diff --git a/uip/mhlogin.c b/uip/mhlogin.c
new file mode 100644
index 0000000..4fa10e1
--- /dev/null
+++ b/uip/mhlogin.c
@@ -0,0 +1,162 @@
+/*
+ * mhlogin.c -- login to external (OAuth) services
+ *
+ * This code is Copyright (c) 2014, by the authors of nmh.  See the
+ * COPYRIGHT file in the root directory of the nmh distribution for
+ * complete copyright information.
+ */
+
+#include <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
+}
diff --git a/uip/msgchk.c b/uip/msgchk.c
index 49e758f..ef9d32b 100644
--- a/uip/msgchk.c
+++ b/uip/msgchk.c
@@ -33,6 +33,7 @@
     X("snoop", -5, SNOOPSW) \
     X("sasl", SASLminc(-4), SASLSW) \
     X("saslmech", SASLminc(-5), SASLMECHSW) \
+    X("oauth service", 0, OAUTHSW) \
     X("proxy command", 0, PROXYSW) \
 
 #define X(sw, minchars, id) id,
@@ -71,7 +72,7 @@ DEFINE_SWITCH_ARRAY(MSGCHK, switches);
 static int donote (char *, int);
 static int checkmail (char *, char *, int, int, int);
 static int remotemail (char *, char *, char *, char *, int, int, int, int,
-       char *);
+       char *, const char *);
 
 
 int
@@ -84,6 +85,7 @@ main (int argc, char **argv)
     char buf[BUFSIZ], *saslmech = NULL;
     char **argp, **arguments, *vec[MAXVEC];
     struct passwd *pw;
+    const char *oauth_svc = NULL;
 
     if (nmh_init(argv[0], 1)) { return 1; }
 
@@ -138,6 +140,16 @@ main (int argc, char **argv)
  adios (NULL, "missing argument to %s", argp[-2]);
  continue;
 
+            case OAUTHSW:
+#ifdef OAUTH_SUPPORT
+                if (!(cp = *argp++) || *cp == '-')
+                    adios (NULL, "missing argument to %s", argp[-2]);
+                oauth_svc = cp;
+#else
+                adios (NULL, "not built with OAuth support");
+#endif
+                continue;
+
  case USERSW:
     if (!(cp = *argp++) || *cp == '-')
  adios (NULL, "missing argument to %s", argp[-2]);
@@ -192,11 +204,11 @@ main (int argc, char **argv)
     if (host) {
  if (vecp == 0) {
     status = remotemail (host, port, user, proxy, notifysw, 1,
- snoop, sasl, saslmech);
+ snoop, sasl, saslmech, oauth_svc);
  } else {
     for (vecp = 0; vec[vecp]; vecp++)
  status += remotemail (host, port, vec[vecp], proxy, notifysw, 0,
-      snoop, sasl, saslmech);
+      snoop, sasl, saslmech, oauth_svc);
  }
     } else {
  if (user == NULL) user = getusername ();
@@ -320,15 +332,24 @@ extern char response[];
 
 static int
 remotemail (char *host, char *port, char *user, char *proxy, int notifysw,
-    int personal, int snoop, int sasl, char *saslmech)
+    int personal, int snoop, int sasl, char *saslmech,
+    const char *oauth_svc)
 {
     int nmsgs, nbytes, status;
     struct nmh_creds creds = { 0, 0, 0 };
 
+    if (oauth_svc == NULL) {
+ nmh_get_credentials (host, user, sasl, &creds);
+    } else {
+ if (user == NULL) {
+    adios (NULL, "must specify -user with -oauth");
+ }
+ creds.user = user;
+    }
+
     /* open the POP connection */
-    nmh_get_credentials (host, user, sasl, &creds);
     if (pop_init (host, port, creds.user, creds.password, proxy, snoop, sasl,
-  saslmech) == NOTOK
+  saslmech, oauth_svc) == NOTOK
     || pop_stat (&nmsgs, &nbytes) == NOTOK     /* check for messages  */
     || pop_quit () == NOTOK) {                 /* quit POP connection */
  advise (NULL, "%s", response);
diff --git a/uip/popsbr.c b/uip/popsbr.c
index 15d03f0..8e1ba2c 100644
--- a/uip/popsbr.c
+++ b/uip/popsbr.c
@@ -8,6 +8,7 @@
 
 #include <h/mh.h>
 #include <h/utils.h>
+#include <h/oauth.h>
 
 #ifdef CYRUS_SASL
 # include <sasl/sasl.h>
@@ -80,34 +81,10 @@ static int sasl_getline (char *, int, FILE *);
 static int putline (char *, FILE *);
 
 
-#ifdef CYRUS_SASL
-/*
- * This function implements the AUTH command for various SASL mechanisms
- *
- * We do the whole SASL dialog here.  If this completes, then we've
- * authenticated successfully and have (possibly) negotiated a security
- * layer.
- */
-
-#define CHECKB64SIZE(insize, outbuf, outsize) \
-    { size_t wantout = (((insize + 2) / 3) * 4) + 32; \
-      if (wantout > outsize) { \
-          outbuf = mh_xrealloc(outbuf, outsize = wantout); \
-      } \
-    }
-
 int
-pop_auth_sasl(char *user, char *host, char *mech)
+check_mech(char *server_mechs, size_t server_mechs_size, char *mech)
 {
-    int result, status, sasl_capability = 0;
-    unsigned int buflen, outlen;
-    char server_mechs[256], *buf, *outbuf = NULL;
-    size_t outbufsize = 0;
-    const char *chosen_mech;
-    sasl_security_properties_t secprops;
-    struct pass_context p_context;
-    sasl_ssf_t *ssf;
-    int *moutbuf;
+  int status, sasl_capability = 0;
 
     /*
      * First off, we're going to send the CAPA command to see if we can
@@ -137,7 +114,7 @@ pop_auth_sasl(char *user, char *host, char *mech)
  * We've seen the SASL capability.  Grab the mech list
  */
  sasl_capability++;
- strncpy(server_mechs, response + 5, sizeof(server_mechs));
+ strncpy(server_mechs, response + 5, server_mechs_size);
     }
     break;
  }
@@ -159,6 +136,42 @@ pop_auth_sasl(char *user, char *host, char *mech)
  return NOTOK;
     }
 
+    return OK;
+}
+
+#ifdef CYRUS_SASL
+/*
+ * This function implements the AUTH command for various SASL mechanisms
+ *
+ * We do the whole SASL dialog here.  If this completes, then we've
+ * authenticated successfully and have (possibly) negotiated a security
+ * layer.
+ */
+
+#define CHECKB64SIZE(insize, outbuf, outsize) \
+    { size_t wantout = (((insize + 2) / 3) * 4) + 32; \
+      if (wantout > outsize) { \
+          outbuf = mh_xrealloc(outbuf, outsize = wantout); \
+      } \
+    }
+
+int
+pop_auth_sasl(char *user, char *host, char *mech)
+{
+    int result, status;
+    unsigned int buflen, outlen;
+    char server_mechs[256], *buf, *outbuf = NULL;
+    size_t outbufsize = 0;
+    const char *chosen_mech;
+    sasl_security_properties_t secprops;
+    struct pass_context p_context;
+    sasl_ssf_t *ssf;
+    int *moutbuf;
+
+    if ((status = check_mech(server_mechs, sizeof(server_mechs), mech)) != OK) {
+ return status;
+    }
+
     /*
      * Start the SASL process.  First off, initialize the SASL library.
      */
@@ -422,6 +435,26 @@ sasl_get_pass(sasl_conn_t *conn, void *context, int id, sasl_secret_t **psecret)
 }
 #endif /* CYRUS_SASL */
 
+int
+pop_auth_xoauth(const char *client_res)
+{
+    char server_mechs[256];
+    int status = check_mech(server_mechs, sizeof(server_mechs), "XOAUTH");
+
+    if (status != OK) return status;
+
+    if ((status = command("AUTH XOAUTH2 %s", client_res)) != OK) {
+      return status;
+    }
+    if (strncmp(response, "+OK", 3) == 0) {
+ return OK;
+    }
+
+    /* response contains base64-encoded JSON, which is always the same.
+     * See mts/smtp/smtp.c for more notes on that. */
+    /* Then we're supposed to send an empty response ("\r\n"). */
+    return command("");
+}
 
 /*
  * Split string containing proxy command into an array of arguments
@@ -473,15 +506,26 @@ parse_proxy(char *proxy, char *host)
 
 int
 pop_init (char *host, char *port, char *user, char *pass, char *proxy,
-  int snoop, int sasl, char *mech)
+  int snoop, int sasl, char *mech, const char *oauth_svc)
 {
     int fd1, fd2;
     char buffer[BUFSIZ];
+    const char *xoauth_client_res = NULL;
 #ifndef CYRUS_SASL
     NMH_UNUSED (sasl);
     NMH_UNUSED (mech);
 #endif /* ! CYRUS_SASL */
 
+#ifdef OAUTH_SUPPORT
+    if (oauth_svc != NULL) {
+ xoauth_client_res = mh_oauth_do_xoauth(user, oauth_svc,
+       snoop ? stderr : NULL);
+    }
+#else
+    NMH_UNUSED (oauth_svc);
+    NMH_UNUSED (xoauth_client_res);
+#endif /* OAUTH_SUPPORT */
+
     if (proxy && *proxy) {
        int pid;
        int inpipe[2];  /* for reading from the server */
@@ -565,6 +609,12 @@ pop_init (char *host, char *port, char *user, char *pass, char *proxy,
  return OK;
  } else
 #  endif /* CYRUS_SASL */
+#  if OAUTH_SUPPORT
+ if (xoauth_client_res != NULL) {
+    if (pop_auth_xoauth(xoauth_client_res) != NOTOK)
+ return OK;
+ } else
+#  endif /* OAUTH_SUPPORT */
  if (command ("USER %s", user) != NOTOK
     && command ("%s %s", (pophack++, "PASS"),
  pass) != NOTOK)
diff --git a/uip/post.c b/uip/post.c
index 685129b..0beab55 100644
--- a/uip/post.c
+++ b/uip/post.c
@@ -80,6 +80,7 @@
     X("nosasl", SASLminc(-6), NOSASLSW) \
     X("saslmaxssf", SASLminc(-10), SASLMXSSFSW) \
     X("saslmech", SASLminc(-5), SASLMECHSW) \
+    X("oauth", -5, OAUTHSW) \
     X("user", SASLminc(-4), USERSW) \
     X("port server submission port name/number", 4, PORTSW) \
     X("tls", TLSminc(-3), TLSSW) \
@@ -256,14 +257,14 @@ static void anno (void);
 static int annoaux (struct mailname *);
 static void insert_fcc (struct headers *, char *);
 static void make_bcc_file (int);
-static void verify_all_addresses (int, char *);
+static void verify_all_addresses (int, char *, const char *);
 static void chkadr (void);
 static void sigon (void);
 static void sigoff (void);
 static void p_refile (char *);
 static void fcc (char *, char *);
 static void die (char *, char *, ...);
-static void post (char *, int, int, char *);
+static void post (char *, int, int, char *, const char *);
 static void do_text (char *file, int fd);
 static void do_an_address (struct mailname *, int);
 static void do_addresses (int, int);
@@ -278,6 +279,7 @@ main (int argc, char **argv)
     char buf[BUFSIZ], name[NAMESZ];
     FILE *in, *out;
     m_getfld_state_t gstate = 0;
+    char *xoauth_client_res = NULL;
 
     if (nmh_init(argv[0], 0 /* use context_foil() */)) { return 1; }
 
@@ -438,7 +440,13 @@ main (int argc, char **argv)
     if (!(saslmech = *argp++) || *saslmech == '-')
  adios (NULL, "missing argument to %s", argp[-2]);
     continue;
-
+
+ case OAUTHSW:
+    if (!(cp = *argp++) || *cp == '-')
+ adios (NULL, "missing argument to %s", argp[-2]);
+                    xoauth_client_res = cp;
+    continue;
+
  case USERSW:
     if (!(user = *argp++) || *user == '-')
  adios (NULL, "missing argument to %s", argp[-2]);
@@ -620,7 +628,7 @@ main (int argc, char **argv)
     /* If we are doing a "whom" check */
     if (whomsw) {
  /* This won't work with MTS_SENDMAIL_PIPE. */
- verify_all_addresses (1, envelope);
+        verify_all_addresses (1, envelope, xoauth_client_res);
  done (0);
     }
 
@@ -632,14 +640,14 @@ main (int argc, char **argv)
    verify_all_addresses with MTS_SENDMAIL_PIPE, but
    that might require running sendmail as root.  Note
    that spost didn't verify addresses. */
- verify_all_addresses (verbose, envelope);
+ verify_all_addresses (verbose, envelope, xoauth_client_res);
     }
-    post (tmpfil, 0, verbose, envelope);
+    post (tmpfil, 0, verbose, envelope, xoauth_client_res);
  }
- post (bccfil, 1, verbose, envelope);
+ post (bccfil, 1, verbose, envelope, xoauth_client_res);
  (void) m_unlink (bccfil);
     } else {
- post (tmpfil, 0, isatty (1), envelope);
+ post (tmpfil, 0, isatty (1), envelope, xoauth_client_res);
     }
 
     p_refile (tmpfil);
@@ -1486,7 +1494,8 @@ do_addresses (int bccque, int talk)
  */
 
 static void
-post (char *file, int bccque, int talk, char *envelope)
+post (char *file, int bccque, int talk, char *envelope,
+      const char *xoauth_client_res)
 {
     int fd;
     int retval, i;
@@ -1536,8 +1545,8 @@ post (char *file, int bccque, int talk, char *envelope)
     } else {
         if (rp_isbad (retval = sm_init (clientsw, serversw, port, watch,
                                         verbose, snoop, sasl, saslssf,
- saslmech, user, tls))  ||
-            rp_isbad (retval = sm_winit (envelope)))
+ saslmech, user, xoauth_client_res, tls))
+            || rp_isbad (retval = sm_winit (envelope)))
     die (NULL, "problem initializing server; %s", rp_string (retval));
 
         do_addresses (bccque, talk && verbose);
@@ -1566,7 +1575,7 @@ post (char *file, int bccque, int talk, char *envelope)
 /* Address Verification */
 
 static void
-verify_all_addresses (int talk, char *envelope)
+verify_all_addresses (int talk, char *envelope, const char *xoauth_client_res)
 {
     int retval;
     struct mailname *lp;
@@ -1576,7 +1585,7 @@ verify_all_addresses (int talk, char *envelope)
     if (!whomsw || checksw)
  if (rp_isbad (retval = sm_init (clientsw, serversw, port, watch,
  verbose, snoop, sasl, saslssf,
- saslmech, user, tls))
+ saslmech, user, xoauth_client_res, tls))
  || rp_isbad (retval = sm_winit (envelope)))
     die (NULL, "problem initializing server; %s", rp_string (retval));
 
diff --git a/uip/send.c b/uip/send.c
index 5885c98..d2bfc62 100644
--- a/uip/send.c
+++ b/uip/send.c
@@ -10,6 +10,8 @@
 #include <h/mh.h>
 #include <fcntl.h>
 
+#include <h/oauth.h>
+#include <h/utils.h>
 
 #ifndef CYRUS_SASL
 # define SASLminc(a) (a)
@@ -61,6 +63,7 @@
     X("nosasl", SASLminc(-6), NOSASLSW) \
     X("saslmaxssf", SASLminc(-10), SASLMXSSFSW) \
     X("saslmech mechanism", SASLminc(-5), SASLMECHSW) \
+    X("oauth service", 0, OAUTHSW) \
     X("user username", SASLminc(-4), USERSW) \
     X("attach", -6, ATTACHSW) \
     X("noattach", -8, NOATTACHSW) \
@@ -115,8 +118,10 @@ main (int argc, char **argv)
     char *cp, *dfolder = NULL, *maildir = NULL;
     char buf[BUFSIZ], **ap, **argp, **arguments, *program;
     char *msgs[MAXARGS], **vec;
+    const char *user = NULL, *oauth_svc = NULL;
     struct msgs *mp;
     struct stat st;
+    int snoop = 0;
 
     if (nmh_init(argv[0], 1)) { return 1; }
 
@@ -227,6 +232,11 @@ main (int argc, char **argv)
     vec[vecp++] = --cp;
     continue;
 
+ case SNOOPSW:
+                    snoop++;
+    vec[vecp++] = --cp;
+    continue;
+
  case DEBUGSW:
     debugsw++; /* fall */
  case NFILTSW:
@@ -238,7 +248,6 @@ main (int argc, char **argv)
  case NMSGDSW:
  case WATCSW:
  case NWATCSW:
- case SNOOPSW:
  case SASLSW:
  case NOSASLSW:
  case TLSSW:
@@ -247,6 +256,25 @@ main (int argc, char **argv)
     vec[vecp++] = --cp;
     continue;
 
+                case OAUTHSW:
+#ifdef OAUTH_SUPPORT
+    if (!(cp = *argp++) || *cp == '-')
+ adios (NULL, "missing argument to %s", argp[-2]);
+                    oauth_svc = cp;
+#else
+                    NMH_UNUSED (oauth_svc);
+                    adios (NULL, "not built with OAuth support");
+#endif
+    continue;
+
+ case USERSW:
+    vec[vecp++] = --cp;
+    if (!(cp = *argp++) || *cp == '-')
+ adios (NULL, "missing argument to %s", argp[-2]);
+    vec[vecp++] = cp;
+                    user = cp;
+    continue;
+
  case ALIASW:
  case FILTSW:
  case WIDTHSW:
@@ -254,7 +282,6 @@ main (int argc, char **argv)
  case SERVSW:
  case SASLMECHSW:
  case SASLMXSSFSW:
- case USERSW:
  case PORTSW:
  case MTSSW:
  case MESSAGEIDSW:
@@ -416,6 +443,18 @@ go_to_it:
  distfile = NULL;
     }
 
+#ifdef OAUTH_SUPPORT
+    if (oauth_svc != NULL) {
+        if (user == NULL) {
+            adios (NULL, "must specify -user with -oauth");
+        }
+
+        vec[vecp++] = "-oauth";
+        vec[vecp++] = mh_oauth_do_xoauth (user, oauth_svc,
+  snoop ? stderr : NULL);
+    }
+#endif /* OAUTH_SUPPORT */
+
     if (altmsg == NULL || stat (altmsg, &st) == NOTOK) {
  st.st_mtime = 0;
  st.st_dev = 0;
--
2.1.3


_______________________________________________
Nmh-workers mailing list
[hidden email]
https://lists.nongnu.org/mailman/listinfo/nmh-workers
Reply | Threaded
Open this post in threaded view
|

Re: Big patch: Add XOAUTH2 support for SMTP and POP

David Levine-3
Eric wrote:

> OAuth is a very modern thing to bring into nmh.

Wow, cool, thanks!

> I enable a libcurl dependency only when configured with
> --with-oauth which is off by default.  But practically no one has
> jsmn installed, so I'm suggesting we include it directly.
> I think that might be unprecedented for nmh.  But I hope it's not
> too controversial.

I think that's fine.

> - mhlogin name / flag names
>
>   Naming is hard :).  I picked this on the theory that it's not
>   terribly confusing as is, and if there were to be some other
>   kind of system users might need to login to, expanding mhlogin
>   to have more than just -oauth would make sense.

I like it.

> - Repeating -user for each command is possibly odd.  Maybe put
>   -user on mhlogin and save it in the cred file.  Arguably easier
>   -for the user this way, arguably not.  Changing it would
>   -complicate the code slightly.  I don't really care either way.

Would it be easier with multiple accounts to use -user everywhere?
I'm thinking shell aliases/scripts.  It could also be done with
the cred file and $MH, but I think that's marginally messier.

> - I have a lot of test cases in only a few broadly categorized
>   test scripts, and they print descriptions as they go so it's
>   easy to see what broke.  This messes up the test suite output.
>   Does this make sense, should I change this only to print only
>   if some environment variable is set,

Or just if something goes wrong?

test-mhlogin failed for me because each line of my actual output
ends with a Ctrl-M.  On Linux, so I don't know why.

Any idea with test-inc and test-msgcheck would fail with:

    'inc: POP server does not support SASL'
    'msgchk: POP server does not support SASL'

I did build with sasl, of course, and confirmed with mhparam.

> Of course, I welcome criticism on all other aspects too:  API,
> documentation, organization, whatever.

I didn't dig in to any of that, but just noticed when applying the
patch:

warning: squelched 5 whitespace errors
warning: 10 lines add whitespace errors.

You chose not to use Cyrus SASL for XOAUTH2.  I wouldn't
hesitate to use it:  nmh already can be configured Cyrus SASL
and some of us do use it.  It is configured in by the Fedora
package.

To do:

* add "mhparam oauth" support
* add libcurl and libcurl-devel (on Linux) to MACHINES
* add reference to jsmn LICENSE to COPYRIGHT

David

_______________________________________________
Nmh-workers mailing list
[hidden email]
https://lists.nongnu.org/mailman/listinfo/nmh-workers
Reply | Threaded
Open this post in threaded view
|

Re: Big patch: Add XOAUTH2 support for SMTP and POP

Eric Gillespie
David Levine <[hidden email]> writes:

> test-mhlogin failed for me because each line of my actual output
> ends with a Ctrl-M.  On Linux, so I don't know why.

Did you use git am?  I just checked and found that plain old
patch(1) preserves the CRs in the tests, but git am loses them.
That's a shame.  Probably standard version control line ending
massaging involved here.  Oh well; guess I'll have to fix this in
fakehttp.c after all.

> Any idea with test-inc and test-msgcheck would fail with:
>
>     'inc: POP server does not support SASL'
>     'msgchk: POP server does not support SASL'
>
> I did build with sasl, of course, and confirmed with mhparam.

You don't need to build with SASL.  I wonder if make check
rebuilds everything?  Maybe you need to make clean first?
Not sure if the dependencies are right.  I'll see if I can
reproduce this.

> warning: squelched 5 whitespace errors
> warning: 10 lines add whitespace errors.

They seem to fall into 3 categories:

- in jsmn

  I'd rather leave this pristine.

- trailing space in an error message

  The error message really has that trailing space on FreeBSD.
  I forgot to highlight this issue earlier, but the 'refresh
  finds no http server' in test-send tests an error message from
  curl that appears on Linux but not FreeBSD.  I'm going to dig
  into curl to see if I can figure out what's going on, but it
  means I'm not entirely sure the test suite is correct on
  all platforms.

- trailing space in send.c that I moved to another line

  removed

> You chose not to use Cyrus SASL for XOAUTH2.  I wouldn't
> hesitate to use it:  nmh already can be configured Cyrus SASL
> and some of us do use it.  It is configured in by the Fedora
> package.

The SASL interaction here is super trivial.  It's the tiniest
part of the patch.  I wouldn't even know where to begin to do
what I've done with Cyrus instead.  And I'd be astonished if that
version wasn't bigger and more complicated.  It's so tiny I'll
include it right here:

    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;
        }
    }

[...]

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;
}

> To do:

I'll take care of these and come back with another patch after
the first round of feedback.

Thanks!

_______________________________________________
Nmh-workers mailing list
[hidden email]
https://lists.nongnu.org/mailman/listinfo/nmh-workers
Reply | Threaded
Open this post in threaded view
|

Re: Big patch: Add XOAUTH2 support for SMTP and POP

David Levine-3
Eric wrote:

> David Levine <[hidden email]> writes:
>
> > test-mhlogin failed for me because each line of my actual output
> > ends with a Ctrl-M.  On Linux, so I don't know why.
>
> Did you use git am?

Yes.

> > Any idea with test-inc and test-msgcheck would fail with:
> >
> >     'inc: POP server does not support SASL'
> >     'msgchk: POP server does not support SASL'
> >
> > I did build with sasl, of course, and confirmed with mhparam.
>
> You don't need to build with SASL.  I wonder if make check
> rebuilds everything?  Maybe you need to make clean first?
> Not sure if the dependencies are right.  I'll see if I can
> reproduce this.

I think you caught it:  I ran the individual tests without
make check.  When I use that, I just get the line-ending
problem.  Also this diff, which I also saw before but didn't
mention:

    ! User-Agent: nmh/1.6+dev libcurl/7.32.0

    ! User-Agent: nmh/

> > warning: squelched 5 whitespace errors
> > warning: 10 lines add whitespace errors.
>
> They seem to fall into 3 categories:
>
> - in jsmn
>
>   I'd rather leave this pristine.

That's fine.

> - trailing space in an error message
>
>   The error message really has that trailing space on FreeBSD.
>   I forgot to highlight this issue earlier, but the 'refresh
>   finds no http server' in test-send tests an error message from
>   curl that appears on Linux but not FreeBSD.  I'm going to dig
>   into curl to see if I can figure out what's going on, but it
>   means I'm not entirely sure the test suite is correct on
>   all platforms.

I don't know that we'll ever be entirely sure.

> - trailing space in send.c that I moved to another line
>
>   removed
>
> > You chose not to use Cyrus SASL for XOAUTH2.  I wouldn't
> > hesitate to use it:  nmh already can be configured Cyrus SASL
> > and some of us do use it.  It is configured in by the Fedora
> > package.
>
> The SASL interaction here is super trivial.  It's the tiniest
> part of the patch.  I wouldn't even know where to begin to do
> what I've done with Cyrus instead.  And I'd be astonished if that
> version wasn't bigger and more complicated.

OK, I (or more likely, Ken) will look at this later.

I also wonder if there's a way to have TLS connection such that
the proxy wouldn't be needed.  We've already got it for outgoing
with send/post -initialtls.

Do you have commit access to the nmh git repo?  This could/
should go in on a branch.

David

_______________________________________________
Nmh-workers mailing list
[hidden email]
https://lists.nongnu.org/mailman/listinfo/nmh-workers
Reply | Threaded
Open this post in threaded view
|

Re: Big patch: Add XOAUTH2 support for SMTP and POP

Ken Hornstein-2
In reply to this post by Eric Gillespie
>I guess this is a bit of a distraction from the current focus on
>improving MIME support and such, and I'm not helping with that
>at all.  Sorry; this is a blocker for me, that's not :(

Hey, I think that is fine; I mean, it's not like we can force you to
work on MIME support, right?  Implementing things that personally affect
you is how SASL support got in there in the first place.

First off, thank you for this awesome work.  It will be great to be
AHEAD of the curve for once.  Nothing jumps right out at me that is
a huge problem (I agree with both a dependency on libcurl and including
our own JSON parser).  I was kind of hoping that this would have been
implemented in Cyrus-SASL and we would have gotten it for free, but
that doesn't seem to be happening so I think implementing this ourselves
makes sense.

I hope you don't mind that I'm going to sit down and digest the details
of the OAuth protocol before I comment further; one problem I always had
was that all of the documentation on it (including the Google ones) were
so web-focused that it never was clear to me how you were supposed to
use it for non-web based protocols.  Hopefully this will clear up some
of the confusion.

I was under the impression you already have access to our git repo; would
you be willing to commit this on a branch right now?  Then it would be easier
for other people to look at it.

--Ken

_______________________________________________
Nmh-workers mailing list
[hidden email]
https://lists.nongnu.org/mailman/listinfo/nmh-workers
Reply | Threaded
Open this post in threaded view
|

Re: Big patch: Add XOAUTH2 support for SMTP and POP

Lyndon Nerenberg (VE6BBM/VE7TFX)
In reply to this post by Eric Gillespie

> Technically, XOAUTH2 is a SASL auth mechanism, but the implementation is so
> trivial, I can't justify the code complexity or additional dependency
> requirement of using Cyrus SASL for this.  So it's completely separate.

While I appreciate the effort put in to this work, I don't think the above statement is justification for putting this code into nmh when it really does belong in the external SASL provider libraries.

Eric, have you approached the Cyrus people to see if they will accept the code?  I can't think of any reason why they wouldn't.

The potential for code conflict here is pretty high.  What happens when the Cyrus SASL library grows its own OAUTH capabilities?  Most likely we would just end up ripping this code out again.  It would be much better if the OAUTH code was incorporated in the correct place to begin with (i.e. Cyrus).

--lyndon


_______________________________________________
Nmh-workers mailing list
[hidden email]
https://lists.nongnu.org/mailman/listinfo/nmh-workers

signature.asc (817 bytes) Download Attachment
Reply | Threaded
Open this post in threaded view
|

Re: Big patch: Add XOAUTH2 support for SMTP and POP

Ken Hornstein-2
>While I appreciate the effort put in to this work, I don't think the
>above statement is justification for putting this code into nmh when it
>really does belong in the external SASL provider libraries.
>
>Eric, have you approached the Cyrus people to see if they will accept
>the code?  I can't think of any reason why they wouldn't.

I looked on the SASL mailing lists; it's not clear to me who's
responsible for developing Cyrus SASL anymore, but there hasn't been a
release in 3 years (okay, my bad; there was a stealth 2.1.26 that was
released in 2012.  So, two years?).  There was someone working on OAUTH
for it here:

  http://lists.andrew.cmu.edu/pipermail/cyrus-sasl/2014-May/002726.html

But ... the overall impression I get is that Cyrus-SASL is kind of
languishing.  Also, from what I've seen at the first glance of Eric's code,
putting it into Cyrus-SASL is going to require some significant work
since there's extra stuff you need to do to before you login that doesn't
quite jibe with the SASL library implementation.

>The potential for code conflict here is pretty high.  What happens when
>the Cyrus SASL library grows its own OAUTH capabilities?  Most likely we
>would just end up ripping this code out again.  It would be much better
>if the OAUTH code was incorporated in the correct place to begin with
>(i.e. Cyrus).

Sigh.  I understand where you're coming from ... but I'll be honest.  My
dealings with the Cyrus SASL people haven't always been positive (okay,
that was a while ago, but still ... they're not so open).  My glance at
Eric's code is that it is reasonable (but I still need to look more closely);
if the choice is wait for someone to put this in Cyrus-SASL, or cram Eric's
code into nmh, well, I'm 100% voting for taking Eric's code into nmh.

--Ken

_______________________________________________
Nmh-workers mailing list
[hidden email]
https://lists.nongnu.org/mailman/listinfo/nmh-workers
Reply | Threaded
Open this post in threaded view
|

Re: Big patch: Add XOAUTH2 support for SMTP and POP

Lyndon Nerenberg (VE6BBM/VE7TFX)

On Dec 6, 2014, at 1:17 PM, Ken Hornstein <[hidden email]> wrote:

> Sigh.  I understand where you're coming from ... but I'll be honest.  My
> dealings with the Cyrus SASL people haven't always been positive (okay,
> that was a while ago, but still ... they're not so open).  My glance at
> Eric's code is that it is reasonable (but I still need to look more closely);
> if the choice is wait for someone to put this in Cyrus-SASL, or cram Eric's
> code into nmh, well, I'm 100% voting for taking Eric's code into nmh.

I will ping the CMU folks and find out what the scoop is.

But, if the Cyrus code is no longer viable, we should be divorcing ourselves from it anyway.  For the basic use we make of SASL, implementing our own client-side SASL support would be pretty straight forward.  The CMU code is license compatible, so we could lift what we need from there as a starting point.

--lyndon


_______________________________________________
Nmh-workers mailing list
[hidden email]
https://lists.nongnu.org/mailman/listinfo/nmh-workers

signature.asc (817 bytes) Download Attachment
Reply | Threaded
Open this post in threaded view
|

Re: Big patch: Add XOAUTH2 support for SMTP and POP

Ken Hornstein-2
>But, if the Cyrus code is no longer viable, we should be divorcing
>ourselves from it anyway.  For the basic use we make of SASL,
>implementing our own client-side SASL support would be pretty straight
>forward.  The CMU code is license compatible, so we could lift what we
>need from there as a starting point.

Ever looked at what it takes to implement the GSSAPI mechanism?  Including
encryption?  It ain't so simple.

--Ken

_______________________________________________
Nmh-workers mailing list
[hidden email]
https://lists.nongnu.org/mailman/listinfo/nmh-workers
Reply | Threaded
Open this post in threaded view
|

Re: Big patch: Add XOAUTH2 support for SMTP and POP

Eric Gillespie
In reply to this post by Lyndon Nerenberg (VE6BBM/VE7TFX)
Lyndon Nerenberg <[hidden email]> writes:

> Eric, have you approached the Cyrus people to see if they will accept the code?  I can't think of any reason why they wouldn't.

I'm willing to take a shot, but I can't even find their code.
Google search for [cyrus sasl] lands me on a cmu.edu page, and I
can't find it anywhere, though I did find this:

http://asg.web.cmu.edu/cyrus/download/anoncvs.html

Which just points me to cyrusimap.org.  And the best I can find
there is a git repo for their IMAP server.

The post Ken references claims there is at least some form of
OAuth support already in the library, so maybe this isn't
that crazy.  But I don't know.

Especially with threats of a sqlite store for creds :(

Thanks.

_______________________________________________
Nmh-workers mailing list
[hidden email]
https://lists.nongnu.org/mailman/listinfo/nmh-workers
Reply | Threaded
Open this post in threaded view
|

Re: Big patch: Add XOAUTH2 support for SMTP and POP

Ken Hornstein-2
>The post Ken references claims there is at least some form of
>OAuth support already in the library, so maybe this isn't
>that crazy.  But I don't know.

It looked like to me that someone had created a repo for their OAuth
plugin, but they hadn't worked on it in a while.

And yeah, the Cyrus web pages are a TOTAL mess.

--Ken

_______________________________________________
Nmh-workers mailing list
[hidden email]
https://lists.nongnu.org/mailman/listinfo/nmh-workers
Reply | Threaded
Open this post in threaded view
|

Re: Big patch: Add XOAUTH2 support for SMTP and POP

Eric Gillespie
In reply to this post by Ken Hornstein-2
Ken Hornstein <[hidden email]> writes:

> I hope you don't mind that I'm going to sit down and digest the details
> of the OAuth protocol before I comment further; one problem I always had
> was that all of the documentation on it (including the Google ones) were
> so web-focused that it never was clear to me how you were supposed to
> use it for non-web based protocols.  Hopefully this will clear up some
> of the confusion.

Of course, take your time and digest.

The RFC leaves a lot up to individual implementors (e.g. Google,
Facebook, etc.).  One option for installed applications is to
start a local web server and use a URL it serves as the redirect
target passed to the authorization server.  I don't think we want
to integrate a web server, but even if we did, that's not a good
fit for us because many of us access nmh via ssh (I do).

The out-of-band stuff ("urn:ietf:wg:oauth:2.0:oob") isn't
documented anywhere except Google's OAuth2InstalledApp page as
far as I can tell.  I think, like XOAUTH2, it's something Google
does that isn't standardized.

> I was under the impression you already have access to our git repo; would
> you be willing to commit this on a branch right now?  Then it would be easier
> for other people to look at it.

I didn't realize my CVS access carried over, but it looks like
it does!  I was able to add and fetch from a Savannah remote anyway:

0 nmh% git remote -v
savannah        git.sv.gnu.org:/srv/git/nmh.git (fetch)
savannah        git.sv.gnu.org:/srv/git/nmh.git (push)

Is this the command I run to push up my branch?

git push savannah xoauth

My git experience is solely as a frontend to Perforce at work,
managing private repositories at home, and sending out patches
with git format-patch; never actually pushed to a public repo!

Thanks.

_______________________________________________
Nmh-workers mailing list
[hidden email]
https://lists.nongnu.org/mailman/listinfo/nmh-workers
Reply | Threaded
Open this post in threaded view
|

Re: Big patch: Add XOAUTH2 support for SMTP and POP

David Levine-3
Eric wrote:

> Is this the command I run to push up my branch?
>
> git push savannah xoauth

You'll want:

    git push -u savannah xoauth

to make it easier to track going forward.

David

_______________________________________________
Nmh-workers mailing list
[hidden email]
https://lists.nongnu.org/mailman/listinfo/nmh-workers
Reply | Threaded
Open this post in threaded view
|

Re: Big patch: Add XOAUTH2 support for SMTP and POP

Eric Gillespie
David Levine <[hidden email]> writes:

> You'll want:
>
>     git push -u savannah xoauth
>
> to make it easier to track going forward.

Done.  Thanks.

_______________________________________________
Nmh-workers mailing list
[hidden email]
https://lists.nongnu.org/mailman/listinfo/nmh-workers
Reply | Threaded
Open this post in threaded view
|

Re: Big patch: Add XOAUTH2 support for SMTP and POP

Eric Gillespie
In reply to this post by Eric Gillespie
Resurrecting the "unresolved issues" section of this :)

On Fri, Dec 5, 2014 at 6:07 PM, Eric Gillespie <[hidden email]> wrote:

> Unresolved issues:
>
> - mhlogin name / flag names
>
>   Naming is hard :).  I picked this on the theory that it's not
>   terribly confusing as is, and if there were to be some other
>   kind of system users might need to login to, expanding mhlogin
>   to have more than just -oauth would make sense.

I think we decided my names were ok? I don't mind at all if someone
wants to change them now.

> - Repeating -user for each command is possibly odd.  Maybe put
>   -user on mhlogin and save it in the cred file.  Arguably easier
>   -for the user this way, arguably not.  Changing it would
>   -complicate the code slightly.  I don't really care either way.

As I said, this would complicate the code, and I'm not  really sure
it's worth it. So you have -user foo repeated on a few lines in
.mh_profile, so what?

> - I have a lot of test cases in only a few broadly categorized
>   test scripts, and they print descriptions as they go so it's
>   easy to see what broke.  This messes up the test suite output.
>   Does this make sense, should I change this only to print only
>   if some environment variable is set, or should I just break
>   these up into one test case per script?  I'd kinda prefer the
>   latter, but I don't know if anyone objects to a big pile of
>   test scripts in there.

I thought someone had a suggestion for this, but just now I looked for
it and didn't find it.

> - Owning the Google client credentials.  I took the liberty of
>   creating a Google developer project for nmh already.  I'm happy
>   to share ownership, or just give it away, or dispose of mine in
>   favor of another.  I would suggest that the Google project have
>   at least two owners.

Right :)

Also, I never did figure out what to do about different error messages
on different platforms. I work on FreeBSD and Linux and they were
different; never mind what other platforms look like...

Todos suggested by David:

To do:

* add "mhparam oauth" support
* add libcurl and libcurl-devel (on Linux) to MACHINES
* add reference to jsmn LICENSE to COPYRIGHT

Thanks.

_______________________________________________
Nmh-workers mailing list
[hidden email]
https://lists.nongnu.org/mailman/listinfo/nmh-workers
Reply | Threaded
Open this post in threaded view
|

Re: Big patch: Add XOAUTH2 support for SMTP and POP

David Levine-3
Eric wrote:

> I think we decided my names were ok? I don't mind at all if someone
> wants to change them now.

I think they're fine.

> > - Repeating -user for each command is possibly odd.  Maybe put
> >   -user on mhlogin and save it in the cred file.  Arguably easier
> >   -for the user this way, arguably not.  Changing it would
> >   -complicate the code slightly.  I don't really care either way.
>
> As I said, this would complicate the code, and I'm not  really sure
> it's worth it. So you have -user foo repeated on a few lines in
> .mh_profile, so what?

I think that I'll want -user, to support multiple accounts on the
same server.

> > - I have a lot of test cases in only a few broadly categorized
> >   test scripts, and they print descriptions as they go so it's
> >   easy to see what broke.  This messes up the test suite output.
> >   Does this make sense, should I change this only to print only
> >   if some environment variable is set, or should I just break
> >   these up into one test case per script?  I'd kinda prefer the
> >   latter, but I don't know if anyone objects to a big pile of
> >   test scripts in there.
>
> I thought someone had a suggestion for this, but just now I looked for
> it and didn't find it.

I don't recall a suggestion.  However, I just committed (on the
xauth branch) an optional mechanism that quiets the tests when
successful, but outputs the line that identifies the particular
test that failed when that happens.  I think this provides what
you want, but if not I'll take another shot at it.

The only changes to the oauth tests were to replace "echo" with
"start_test", and add one "finish_test" at the end.  start_test
saves the message instead of outputting it, and a trap function
outputs it only on failure.

> Also, I never did figure out what to do about different error messages
> on different platforms. I work on FreeBSD and Linux and they were
> different; never mind what other platforms look like...

We can see what happens after merging to master and running on the
buildbots.  Shouldn't be a big deal.

David

_______________________________________________
Nmh-workers mailing list
[hidden email]
https://lists.nongnu.org/mailman/listinfo/nmh-workers
Reply | Threaded
Open this post in threaded view
|

Re: Big patch: Add XOAUTH2 support for SMTP and POP

Eric Gillespie
I got my computer working again!  Responding with nmh this time,
not Android :)

So let me know if you want me to work on anything here.

David Levine <[hidden email]> writes:

> > > - Repeating -user for each command is possibly odd.  Maybe put
> > >   -user on mhlogin and save it in the cred file.  Arguably easier
> > >   -for the user this way, arguably not.  Changing it would
> > >   -complicate the code slightly.  I don't really care either way.
> >
> > As I said, this would complicate the code, and I'm not  really sure
> > it's worth it. So you have -user foo repeated on a few lines in
> > .mh_profile, so what?
>
> I think that I'll want -user, to support multiple accounts on the
> same server.

Sounds good.

> The only changes to the oauth tests were to replace "echo" with
> "start_test", and add one "finish_test" at the end.  start_test
> saves the message instead of outputting it, and a trap function
> outputs it only on failure.

Oh, I like it.

> > Also, I never did figure out what to do about different error messages
> > on different platforms. I work on FreeBSD and Linux and they were
> > different; never mind what other platforms look like...

I can't remember what this was about; I thought I made sure I
used the same curl version on both systems, but maybe not.
The tests pass for me now on both FreeBSD and Linux.

Thanks.

_______________________________________________
Nmh-workers mailing list
[hidden email]
https://lists.nongnu.org/mailman/listinfo/nmh-workers