Part of a series on sending emails:
Index > PLAIN (@cs.wisc.edu) > XOAUTH2 (@wisc.edu)


SASL XOAUTH2 Mechanism

What is XOAUTH2?

Here is a very personal take that um... you should just take with a grain of salt alright. I have no idea what this thing is either, or what it is doing that Access Tokens don't already. Previously I mentioned this excerpt from Wikipedia which is an interesting take since it actually links to our uni's page (square brackets are my addition):

Microsoft has announced that they would end support for the less secure basic authentication[...] which disables most use of IMAP or POP3 [that includes SMTP.] and requires significant upgrades to support the more secure OAuth2 based authentication in applications in order to continue to use those protocols;[23] some customers have responded by simply shutting off older protocols[24].

Either way...

Now I would say I myself am not totally on board with this -- at least not at face value. Switching to OAuth2 itself is not a danger; OAuth2 is an open framework, after all, defined in the RFCs (I haven't read any, but a simple search brings up RFC 6749). What makes OAuth2 really a pain in the bottom, though, is that you have to go through the whole formal application/service protocol just to get something as simple as allowing you yourself to log in on behalf of you.

(If that sounded weird and utter gibberish, that's precisely my point. I don't get why the only way to get access to something you clearly own by registering an application and authorizing yourself as a client of that application. We're lucky that scripts can help simplify the process, but man does it suck that everyone needs to learn a bit of programming just to send some emails... (Unless they are fine with Thunderbird, which-))

Okay, here's the important bit. Earlier (like, way earlier when I was raving to you about this) I told you that there was this repository that worked out of the box: it wasn't magic. It didn't have some magical code that bypasses the registration process. Rather, it was using Thunderbird's application credentials and it has them built into the config. So that when we run the script it acts as if Thunderbird is requesting access to reading and sending emails on behalf of you (the contract potentially implied here is that the token will be securely stored by Thunderbird) -- when in reality it was just Python, and the tokens are all really just being stored into ordinary files.

But okay, how do we know Thunderbird's presumably secret app tokens? Well here's the cool part... Thunderbird is open source, the significance of which I hopefully do not have to elaborate on. But that means you can literally "steal" their credentials, and pretend to be them when requesting a pair of access/­refresh from Microsoft or whatever! (Well it isn't "stealing" exactly if it is hard-coded, and public in their svn-or-something repository anyways... but do remember that it's theirs, and you should probably be grateful that some volunteer/­contributer took out their time to go through the tedious application process at every major mail provider so you don't have to.)

Not quite magic! That's why I'm explaining to you why it works, and what to make out of it in the event it stops working (as well as when it does work. Just as I said -- it's not magic!)

The XOAUTH2 mechanism

The mechanism we use -- XOAUTH2 -- uses OAuth2 (as you can easily tell from its name). The "X" typically means extension, i.e. non-standard stuff that isn't defined in RFC or the like. (Although there are non-standard-became standard like the application/x-www-form-urlencoded MIME type, the X-Forwarded-For header... we don't talk about those okay.)

Google and Microsoft are the big players, so we will be reading Google's specifications of this. Let's see uh... the string we will be encoding to Base64 today is...

"user=" {User} "^Aauth=Bearer " {Access Token} "^A^A"

.... WUT. is this.

What is with those ^A characters? (Google later explains that they are ASCII \x01, the first control character. But why choose that?) Why are there two ^A characters at the end? And what's with all the "user=" "auth=", this funky Bearer thing, all the extra noise... (man I miss my AUTH PLAIN already, can we go back?)

Okay... but minus the peculiar placement of ^A, I can explain where the rest is coming from: they are directly translated from OAuth2, a protocol that is normally designed to work on the Web, i.e. HTTP. Namely, I can recognize that the "auth=" thing comes from the Authorization header, which says the following:

Authorization: <auth-scheme> <authorization-parameters>

The exact schemes, as well as the parameters they take, are listed in a separate page. And guess what... one of them documents the Bearer scheme: in RFC 6750! Section 2.1 defines the following, somewhat liberal syntax:

     ; Core rules in RFC 5234, Appendix B.1
     ALPHA          =  %x41-5A / %x61-7A   ; A-Z / a-z
     DIGIT          =  %x30-39             ; 0-9
     SP             =  %x20

     b64token    = 1*( ALPHA / DIGIT /
                       "-" / "." / "_" / "~" / "+" / "/" ) *"="
     credentials = "Bearer" 1*SP b64token

(perhaps allowing for URL-safe Base64, which uses for "-" and "_" for 63,64 instead of the usual "+" and "/"? But then why is there the tilde... nevermind I think they're just paranoid =_=)

(And yes -- before you make this observation -- you are encoding a Base64-credential back into Base64. But the access token is usually provided to you in Base64 anyways; you just have to pretend it is binary and encode it anyways. It's weird, but then again, someone had to make a decision that is the least confusing out of all... :)

(And yes again -- in case you know about how OAuth2 generally works -- the token alone is often enough to identify you. So technically OAuth2 could have been just the access token alone. But then someone decided that alone would be un-cool for some reason. (Well I don't see anyone complaining about cookies, but maybe email is a different beast...) I don't know. It's understandable though, since the user effectively serves as an authentication realm/­namespace (if I even know what that is). So it's better to have those than not. The real pain however is that XOAUTH2 tokens will be excruciatingly long to copy -- due to well, obviously, the doubly-encoded Base64 access token (33% inflation, twice!)... but I'm sure you'll get used to gibberish like this, don't worry. ^^)

XOAUTH2 Workflow Overview

We have to pretend to be an OAuth application, basically. There are two steps involved in the flow:

  1. Initial step: upon request for an OAuth access token, we log into our account and approve access to a limited scope of account services as the account owner. (In this case, the scope is IMAP + SMTP access.) The flow terminates as Microsoft returns us a pair of access and refresh tokens.
  2. Subsequent steps: we, without explicit log-in approval, request an up-to-date access token directly from Microsoft using the refresh token.

Once we have a pair of access and refresh tokens, we can take the access token and create the XOAUTH2 credentials as I explained above. The rest is the same as we did with AUTH PLAIN: we just replace PLAIN with XOAUTH2 and enter the string.

Pretty simple, right? So let's work this through...

Sending as @wisc.edu

Let's do the access token part first.

Clone the repository. You can name it whatever you want. I'll call it oauth/wiscmail so I am going to run this:

$ git clone https://github.com/UvA-FNWI/M365-IMAP oauth/wiscmail
# or with an SSH URI...
$ git clone git@github.com:UvA-FNWI/M365-IMAP oauth/wiscmail
$ cd oauth/wiscmail

Depending on what shell you are using, you may be able to type cd $_ or cd !$ to save some typing.

The next step is to install some Python dependencies. They are listed in requirements.txt, so you can run pip install <package>. Of course, there is a much better alternative:

$ pip install -r requirements.txt

If pip is not on your PATH, try prepending python3 -m python -m (or any weirder names Windows might have in store for it).

Of course, it is implied that you use a virtual environment, since the Python Packaging Authority these days is quite finicky about having people not install packages globally alongside the system packages any more, and you would probably be greeted with a screen of terror if you ran that command without one. (Don't worry -- they just really want you to use a virtual environment, so just make one and forget about it.)

Assuming you don't already have a virtual environment you like better, my suggestion is you create one named ".venv" right here in the repository:

python3 -m venv .venv
# I sometimes like virtualenv better
virtualenv .venv

Now based on your shell, source the script that activates the environment:

.      .venv/bin/activate   # Bourne shells
source .venv/bin/activate   # "source" is a common alias of "dot"
venv\Scripts\activate.bat & REM Windows Command Prompt (cmd.exe)
venv\Scripts\Activate.ps1   # Windows PowerShell
# (For other shells (fish, csh, ...) look into venv/bin.)

... and you may now run pip to install the dependencies. :)

(You likely don't have to ever deactivate the environment or switch to another. But in case you do, you should be able to just run deactivate in your shell.)

Notice: I have tried installing on Windows following these exact steps. It would seem that some of the packages might require you to install Microsoft Visual C/C++ (MSVC for short; has the C compiler cl.exe) and a Rust compiler (rustc). Read about the details here. (I should note that MSVC is not equivalent to Visual Studio (the latter is so much more than just MSVC), and to Visual Studio Code to an even lesser extent. The way I roughly understand it is MSVC is to Visual Studio is XCode Command Line Tools is to XCode, in that you shouldn't have to install too much of the latter (or even install it at all) to get the former.)

After you have run "pip install", look inside config.py. Not that there is anything I'd like you to change, but just keep in mind the ClientId and ClientSecret are from Thunderbird. (Which means you owe them one -- well. We all do :)

(Note that you can technically change the file name of the access and refresh tokens, but to be real, I don't see a strong reason to: we'll just keep them at imap_smtp_­refresh_token and imap_smtp_­access_token. Also, in case you are paranoid: Scopes is a list of what this access token will be granted access to. You will have a chance to review this later.)

Now run get_token.py to get an initial pair of access and refresh token. Your browser should open a link to the login page where you can log into the university account. Once you are logged in, you should see a screen that looks like this:

MS365 OAuth flow: Permission requested

Here it translates the scopes into more human terms (and thusly, more verbose). Now you should probably replace "app" with just "token" since there is no app... we are the app, you could say. So this token we are requesting will grant us (without the need to manually log in!) the ability to:

And down at the bottom in very fine print says "You can change these permissions at https://myapps.microsoft.com/." Well that is true -- and I advise you to have it open before you proceed, so that you have a chance to identify which app corresponds to the tokens you hold.

When you accept, you will see a new icon appear in this giant array of icons, where it says "Thunderbird". But remember there is no Thunderbird; it's just you and this get_token.py script: you are the "app" that you just approved! So should you ever lose access to your computer, or you don't want to send emails this way any more, do this:

  1. Click on the three vertical dots at the upper-right corner. There you will see a dropdown menu: click Manage your application. Do not click on the icon itself -- it would not very useful. And especially do not click on Remove: that only hides the app from the dashboard while it maintains access to the given auth scopes.
  2. Once you are in that menu, it should be obvious what to click on next: Revoke consent. (I don't know why they called it that, eh. Clearly the rest of the world calls it "access". But anyway, it is that button that nukes your access/refresh tokens.)

If you accidentally messed up the first step by clicking on Remove instead of Manage your application > Revoke consent, I got a trick for you: go up to Settings with a gear icon to its left, then click Reset to default. I have no idea what "default" here means, but it worked for me. (And if your dashboard is even more messed up than that, look at Customize view > Manage collections and see if you can un-hide an existing default collection or add a new one containing all applications you have. I don't know, this interface was kinda confusing to me :/)

... Okay. I hope it's clear how you can "undo" all this mess. Now you can click that "Accept" button and that first step is done! You should now have an access token stored in imap_smtp_access_token and a refresh token stored in imap_smtp_refresh_token.

Now the access token is ephemeral (valid for about 1 hours I believe) so I can safely show you that. If you run cat imap_smtp_access_token (replace cat with type in MS-DOS cmd.exe, Get-Content in PowerShell (though cat is aliased to that so you technically don't have to)), you will something like this:

eyJ0eXAiOiJKV1QiLCJub25jZSI6IjRHM1JsbmxjM3o1cms0ckEtajZqR1dxYTlfcjIxdklJVlJZMTg1SXBoNEkiLCJhbGciOiJSUzI1NiIsIng1dCI6Il9qTndqZVNudlRUSzhYRWRyNVFVUGtCUkxMbyIsImtpZCI6Il9qTndqZVNudlRUSzhYRWRyNVFVUGtCUkxMbyJ9.eyJhdWQiOiJodHRwczovL291dGxvb2sub2ZmaWNlLmNvbSIsImlzcyI6Imh0dHBzOi8vc3RzLndpbmRvd3MubmV0LzJjYTY4MzIxLTBlZGEtNDkwOC04OGIyLTQyNGE4Y2I0YjBmOS8iLCJpYXQiOjE3NTIzMzk1MjgsIm5iZiI6MTc1MjMzOTUyOCwiZXhwIjoxNzUyMzQ0MzI3LCJhY2N0IjowLCJhY3IiOiIxIiwiYWlvIjoiQVhRQWkvOFpBQUFBeWVWa29GaktQcXlnZVR6SERZZ09xNkkyREZ2bFNsNEpNQk1KM3dzZDhkNXYrZWJXUy9aNGlZZXlseGM1S2k3MUlTdENoc2NONVJIMkJ0REtLTzlETUxFSVJZSjBmeXdkMUp1c3dqOFR5VFRBQzlaOXF2eVFkOEN1RTRhbVJ1Z0M2b1RiY0lqRHhaZ1VhUDFNUEh0R2FnPT0iLCJhbXIiOlsicHdkIl0sImFwcF9kaXNwbGF5bmFtZSI6IlRodW5kZXJiaXJkIiwiYXBwaWQiOiIwODE2MmY3Yy0wZmQyLTQyMDAtYTg0YS1mMjVhNGRiMGI1ODQiLCJhcHBpZGFjciI6IjEiLCJlbmZwb2xpZHMiOltdLCJmYW1pbHlfbmFtZSI6Ik1lbmciLCJnaXZlbl9uYW1lIjoiRXRoYW4iLCJpZHR5cCI6InVzZXIiLCJpcGFkZHIiOiIxMjAuMjI5LjMwLjQiLCJsb2dpbl9oaW50IjoiTy5DaVEzTnpJNU1UVXlZeTFrTm1aa0xUUXlaRFF0T1dVeVlTMHhNVE5sTkRSaE9ETmxZVGdTSkRKallUWTRNekl4TFRCbFpHRXRORGt3T0MwNE9HSXlMVFF5TkdFNFkySTBZakJtT1JvUGJXVnVaelk1UUhkcGMyTXVaV1IxSUdrPSIsIm5hbWUiOiJFdGhhbiBNZW5nIiwib2lkIjoiNzcyOTE1MmMtZDZmZC00MmQ0LTllMmEtMTEzZTQ0YTgzZWE4Iiwib25wcmVtX3NpZCI6IlMtMS01LTIxLTk0NDQ0NTYyOS0xNDg5OTgwNjc4LTE4NDA3NDI2Ny0xOTExOTkwIiwicHVpZCI6IjEwMDMyMDAzNzFBRDM0RTQiLCJyaCI6IjEuQVZjQUlZT21MTm9PQ0VtSXNrSktqTFN3LVFJQUFBQUFBUEVQemdBQUFBQUFBQURQQVhaWEFBLiIsInNjcCI6IklNQVAuQWNjZXNzQXNVc2VyLkFsbCBTTVRQLlNlbmQiLCJzaWQiOiIwMDZjYWM2OS0xOTM1LTYyYmEtNWFmNy0zNjAzNWM4MzIxNWIiLCJzdWIiOiI3aUVvZHFkMjlyZ2tUbk1sWE1uNDJOUV9RZGhvN1JwNTh1ZFk4WDFKVUY0IiwidGlkIjoiMmNhNjgzMjEtMGVkYS00OTA4LTg4YjItNDI0YThjYjRiMGY5IiwidW5pcXVlX25hbWUiOiJtZW5nNjlAd2lzYy5lZHUiLCJ1cG4iOiJtZW5nNjlAd2lzYy5lZHUiLCJ1dGkiOiJlNnhDaUY5N1RVNkdNd2pkN1Z3YkFBIiwidmVyIjoiMS4wIiwid2lkcyI6WyJiNzlmYmY0ZC0zZWY5LTQ2ODktODE0My03NmIxOTRlODU1MDkiXSwieG1zX2F1ZF9ndWlkIjoiMDAwMDAwMDItMDAwMC0wZmYxLWNlMDAtMDAwMDAwMDAwMDAwIiwieG1zX2Z0ZCI6IlhadE4zVEY0QjNlNmI2MWpYOEJyVnNBNnZHVlQyV2NUMFpkRGgzaktQZTRCZFhOemIzVjBhQzFrYzIxeiIsInhtc19pZHJlbCI6IjEgMTIifQ.VjdREaPoiuG_N9jqne2A3nYdYdyBfLqUESOVkKYgNXpvewO4zeDzg9FNciKy3chMxSNThfkIDGAexI97NQ4EVrMoucSooZlEhcqiVTdm2oS8U4rqaPxXRKzPi9-jouqL_HWdXsnPPEMnPVfk7p_BxjUHWayGZDDJXpK3ZVdOgYvRAPH8avUdBGkcMq6mUv611j1H80-iHFKML1PGwLU-enBThDXEiugxQsnGru0lKz50-xoKVYue6X0nnfZh_qJiMhBfIo_Y7p4UY5rQRNfUwLnZrHUEsfIlqDJwX9jaE3ZzsA0rWAL9KM3jUwkU1qdH3J4EPU8agsu-ZE5JEPI0gA

Wow. What a mess, right? Well that is what I would have told you if I never looked inside the string...

Want to look inside the access token? (Just a heads-up, this will be a digression.)

So here's the cool thing: whenever you see "ey", you know it's JSON. Why? Well {" decodes to "ey". (I always thought it was eerie how it looked like my name every time... well, turns out it's just Base64.)

Anyway, what do you get when you decode the string, until you can't anymore?

$ cat imap_smtp_access_token | cut -d. -f1 | tr _- /+ | base64 -d | jq .
{
  "typ": "JWT",
  "nonce": "4G3Rlnlc3z5rk4rA-j6jGWqa9_r21vIIVRY185Iph4I",
  "alg": "RS256",
  "x5t": "_jNwjeSnvTTK8XEdr5QUPkBRLLo",
  "kid": "_jNwjeSnvTTK8XEdr5QUPkBRLLo"
}

What is this, JWT... JSON Web Token (RFC 7519 / jwt.io)! This is the header part of a JWT. The next part is the payload:

$ cat imap_smtp_access_token | cut -d. -f2 | tr _- /+ | base64 -d | jq .
{
  "aud": "https://outlook.office.com",
  "iss": "https://sts.windows.net/2ca68321-0eda-4908-88b2-424a8cb4b0f9/",
  "iat": 1752339528,
  "nbf": 1752339528,
  "exp": 1752344327,
  "acct": 0,
  "acr": "1",
  "aio": "AXQAi/8ZAAAAyeVkoFjKPqygeTzHDYgOq6I2DFvlSl4JMBMJ3wsd8d5v+ebWS/Z4iYeylxc5Ki71IStChscN5RH2BtDKKO9DMLEIRYJ0fywd1Juswj8TyTTAC9Z9qvyQd8CuE4amRugC6oTbcIjDxZgUaP1MPHtGag==",
  "amr": [
    "pwd"
  ],
  "app_displayname": "Thunderbird",
  "appid": "08162f7c-0fd2-4200-a84a-f25a4db0b584",
  "appidacr": "1",
  "enfpolids": [],
  "family_name": "Meng",
  "given_name": "Ethan",
  "idtyp": "user",
  "ipaddr": "120.229.30.4",
  "login_hint": "O.CiQ3NzI5MTUyYy1kNmZkLTQyZDQtOWUyYS0xMTNlNDRhODNlYTgSJDJjYTY4MzIxLTBlZGEtNDkwOC04OGIyLTQyNGE4Y2I0YjBmORoPbWVuZzY5QHdpc2MuZWR1IGk=",
  "name": "Ethan Meng",
  "oid": "7729152c-d6fd-42d4-9e2a-113e44a83ea8",
  "onprem_sid": "S-1-5-21-944445629-1489980678-184074267-1911990",
  "puid": "1003200371AD34E4",
  "rh": "1.AVcAIYOmLNoOCEmIskJKjLSw-QIAAAAAAPEPzgAAAAAAAADPAXZXAA.",
  "scp": "IMAP.AccessAsUser.All SMTP.Send",
  "sid": "006cac69-1935-62ba-5af7-36035c83215b",
  "sub": "7iEodqd29rgkTnMlXMn42NQ_Qdho7Rp58udY8X1JUF4",
  "tid": "2ca68321-0eda-4908-88b2-424a8cb4b0f9",
  "unique_name": "meng69@wisc.edu",
  "upn": "meng69@wisc.edu",
  "uti": "e6xCiF97TU6GMwjd7VwbAA",
  "ver": "1.0",
  "wids": [
    "b79fbf4d-3ef9-4689-8143-76b194e85509"
  ],
  "xms_aud_guid": "00000002-0000-0ff1-ce00-000000000000",
  "xms_ftd": "XZtN3TF4B3e6b61jX8BrVsA6vGVT2WcT0ZdDh3jKPe4BdXNzb3V0aC1kc21z",
  "xms_idrel": "1 12"
}

The part I care about is "exp": 1752344327, which tells me that this token expired a while ago as of writing this:

$ date -d @1752344327 --rfc-email
Sun, 13 Jul 2025 02:18:47 +0800

(And well, you can see the token contains my username, my name, and everything... fair to say that XOAUTH2 is very redundant when the access token already stores all the information. =_=)

The third part is an RSA signature of the SHA256 of the header, payload, and other stuff (you can tell from "alg" being RS256). This time it's real gibberish to us; and moreover, only the person who holds the RSA public key would know what to make of it...

$ cat imap_smtp_access_token | cut -d. -f3 | tr _- /+ | base64 -d | xxd
00000000: 5637 5111 a3e8 8ae1 bf37 d8ea 9ded 80de  V7Q......7......
00000010: 761d 61dc 817c ba94 1123 9590 a620 357a  v.a..|...#... 5z
00000020: 6f7b 03b8 cde0 f383 d14d 7222 b2dd c84c  o{.......Mr"...L
00000030: c523 5385 f908 0c60 1ec4 8f7b 350e 0456  .#S....`...{5..V
00000040: b328 b9c4 a8a1 9944 85ca a255 3766 da84  .(.....D...U7f..
00000050: bc53 8aea 68fc 5744 accf 8bdf a3a2 ea8b  .S..h.WD........
00000060: fc75 9d5e c9cf 3c43 273d 57e4 ee9f c1c6  .u.^..<C'=W.....
00000070: 3507 59ac 8664 30c9 5e92 b765 574e 818b  5.Y..d0.^..eWN..
00000080: d100 f1fc 6af5 1d04 691c 32ae a652 feb5  ....j...i.2..R..
00000090: d63d 47f3 4fa2 1c52 8c2f 53c6 c0b5 3e7a  .=G.O..R./S...>z
000000a0: 7053 8435 c48a e831 42c9 c6ae ed25 2b3e  pS.5...1B....%+>
000000b0: 74fb 1a0a 558b 9ee9 7d27 9df6 61fe a262  t...U...}'..a..b
000000c0: 3210 5f22 8fd8 ee9e 1463 9ad0 44d7 d4c0  2._".....c..D...
000000d0: b9d9 ac75 04b1 f225 a832 705f d8da 1376  ...u...%.2p_...v
000000e0: 73b0 0d2b 5802 fd28 cde3 5309 14d6 a747  s..+X..(..S....G
000000f0: dc9e 043d 4f1a 82cb be64 4e49 10f2 3480  ...=O....dNI..4.

A signature does exactly what you think it does: it ensures that only this exact piece of payload goes with it. If I changed my account name, expiration date... anything at all, the signature becomes invalid. That's why I am comfortable pasting the entire access token here: even I can't tamper with it!

Now of course, refresh token is something else. It doesn't look like a JWT, and it is also much shorter. That is something I feel like if I gave away would cause a lot of trouble. And I can't seem to find meaningful patterns, so... that will be that.

Anyways, whether you expanded the <details> or not: the refresh token is special in that it never expires. That's good because, even if we don't come back every hour or so for a new token, we can come back whenever and use the refresh token to obtain a new token (that invalidates the previous refresh token, I believe... I mean it has to right otherwise you have infinite refresh tokens which is not cool.) That's also bad because unlike access tokens that do expire, revocation is the only real way to invalidate the refresh token. Or maybe you can destroy it yourself so no one can have it, but well... the proper way to do it, still, is to use myapps.microsoft.com to revoke it. (Oh, and if you lose the refresh token... same thing. Revoke and re-authorize.)

Okay. Now it's time to encode the XOAUTH2 string. Same as when we talked about encoding for AUTH PLAIN, there are many ways to do it in Bash *ahem* sorry, Bourne shells:

$ printf 'user=%s\001auth=Bearer %s\001\001' \
  meng69@wisc.edu "$(cat imap_smtp_access_token)" |
  base64 | tr -d '\n' && echo
dXNlcj1tZW5nNjlAd2lzYy5lZHUBYXV0aD1CZWFyZXIgZXlKMGVYQWlPaUpLVjFRaUxDSnViMjVqWlN[... 3177 more bytes]

And obviously I wrote a Java program that does the same... well. Remember I told you to clone this repository at oauth/wiscmail? Well I hope you did that... because we are cloning this one to the top!

$ cd ../..
# Because Git won't let us clone into a non-empty directory, we're
# doing this "hack" where we do the cloning and checkout separately.
$ git clone --bare https://github.com/eyzmeng/sasl-encoder.git .git
# (Again, you can also do the SSH URI)
$ git clone --bare git@github.com:eyzmeng/sasl-encoder.git .git
$ git config core.bare false
$ git switch --detach v1.1.2.1

This peculiar set-up is so that you can directly run the program, XOAuth2SaslSmtpAuthExample.java, so that it reads from oauth/wiscmail/imap_smtp_access_token:

$ javac -d out src/*
$ java -cp out XOAuth2SaslSmtpAuthExample
Using oauth/wiscmail/imap_smtp_access_token as access token.
dXNlcj1tZW5nNjlAd2lzYy5lZHUBYXV0aD1CZWFyZXIgZXlKMGVYQWlPaUpLVjFRaUxDSnViMjVqWlN[... 3177 more bytes]

You should at least open XOAuth2SaslSmtpAuthExample.java and change the USER to your email: this "dXNlcj1tZW5nNjlAd2lzYy5lZHUB" is my email!

CHALLENGE: send an email with SASL XOAUTH2

So I hope you have been expecting this... well. You have everything with you. Let's just do it. :)

Assuming that you have set it up the way I told you:

.
├── oauth
│   └── wiscmail
│       ├── get_token.py
│       ├── refresh_token.py
│       ├── imap_smtp_access_token
│       └── imap_smtp_refresh_token
├── out
│   └── XOAuth2SaslSmtpAuthExample.class
└── src
    └── XOAuth2SaslSmtpAuthExample.java

First, change your working directory into oauth/wiscmail. Then run refresh_token.py with Python. This will update both imap_smtp_access_token and imap_smtp_refresh_token.

You will then want to come back up and run XOAuth2SaslSmtpAuthExample with Java. This will print a ginormous string, which you should remember.

Then what is it, oh... right, the server you will connect to (as well as the port number)! Currently, I have no gosh darn idea, since kb.wisc.edu won't let me connect from China :))) but any one of the following seems to work:

Remember this means that you want to connect with -starttls smtp: (in Windows terminal or cmd.exe, omit -crlf.)

$ openssl s_client -nocommands -crlf \
    -starttls smtp -connect outlook.office365.com:587

Now you should be pretty familiar what to do next: Greet it with an EHLO... authenticate with AUTH XOAUTH this time! Then paste the ginormous string that you have hopefully saved. And you are authenticated. Same stuff.

Though for this time, *I* am doing it differently: I'm just gonna send it the usual way I prepare and send emails. And well, let's just say... this is how it looks like to me:

$ (cd "$(git rev-parse --show-toplevel)" && tree compose/2025/07/13)
compose/2025/07/13
├── compose.pl -> ../../../2025/03/23/compose.pl
├── queue.pl -> ../../../2025/03/23/queue.pl
├── sendmail.sh -> ../../../2025/03/23/sendmail.sh
├── xoauth2
├── xoauth2.conf
└── xoauth2.d
    ├── compose
    │   └── mexmid.pl
    ├── dat_e
    ├── date
    ├── mid
    ├── mid_e
    └── text

3 directories, 11 files
$ ./sendmail.sh xoauth2
+ ./compose.pl xoauth2
Wrote 'Mon, 14 Jul 2025 01:01:36 +0800' to 'xoauth2.d/date'
Wrote '<1752426096.404415.E5158AebBD27f@e.rapidcow.org>' to 'xoauth2.d/mid'
+ for cp in "$name.d"/compose/*
+ '[' -x xoauth2.d/compose/mexmid.pl ']'
+ xoauth2.d/compose/mexmid.pl xoauth2
+ compose.py -y xoauth2.conf -o xoauth2
Composed 3709 bytes in 'xoauth2'
Would you to preview? (y/[n]) y
+ vim '+set filetype=mail' -R xoauth2

I didn't mean to press that, but never mind...

MAIL FROM:<meng69@wisc.edu>
+ exec ./queue.pl xoauth2
You are sending: xoauth2
RCPT TO:<~rapidcow/wiscmail-inbox@lists.sr.ht>
Does this look right? (y/[n]) y
SEND Date: Mon, 14 Jul 2025 01:01:36 +0800
SEND Message-ID: <1752426096.auth.xoauth2.example@e.rapidcow.org>
SEND From: Ethan Meng <ethan.meng@wisc.edu>
SEND To: ~rapidcow/wiscmail-inbox@lists.sr.ht
SEND Subject: Sending with AUTH XOAUTH2
SEND MIME-Version: 1.0
SEND Content-Type: text/plain; charset="utf-8"
SEND Content-Transfer-Encoding: quoted-printable
SEND 
SEND This is my final test message!
SEND 
SEND I've been waiting for a long long time to tell you this...
SEND but this is how I've been sending email since November. :)
SEND 
SEND Each mail I have sent to you, aside from the one where I was on
SEND the plane, was hand-crafted with scripts this way.  (Check the
SEND Message-ID and you'll know!)
SEND 
SEND What you've been reading is basically what I discovered along
SEND the way... being able to send from a Microsoft email *without*
SEND Outlook is a big feat!  People think getting the OAuth set up
SEND and having to learn a little programming a little is hard; well
SEND I hope I have showed you with a little bit of determination and
SEND knowledge about how things work, you can.  Once again, you can
SEND thank Thunderbird for the client ID and secrets.
SEND 
SEND Of course, this time I am not voluntarily talking to SMTP.
SEND I will be using compose.py to convert from this file:
SEND 
SEND   headers
SEND     Date: < xoauth2.d/dat_e
SEND     Message-ID: < xoauth2.d/mid_e
SEND     From: Ethan Meng <ethan.meng@wisc.edu>
SEND     To: ~rapidcow/wiscmail-inbox@lists.sr.ht
SEND     Subject: Sending with AUTH XOAUTH2
SEND   content
SEND     text/plain < xoauth2.d/text
SEND       with charset utf-8
SEND       of encoding quoted-printable
SEND         where treol =3D please!
SEND 
SEND into the message you will see on the mailing list, sent by my
SEND own queue.pl.  But hopefully now, you can see that through the
SEND tools I developed along the way that my approach is more about
SEND factoring repetitive parts than hiding away the way things work.
SEND 
SEND It is this feeling to know your tools well enough to be able to
SEND take them apart and put them together that I want you to have.
SEND Too often we praise convenience confined to one ecosystem over
SEND boring specifications for open exchange of information; exercise
SEND control by means of black-boxing and technical obscurity than
SEND give back control by means of transparency and... other things.
SEND Like this right here I am writing.
SEND 
SEND That's also why I personally believe it is more important to
SEND learn programming than ever but that's too much self-indulging
SEND opinion in one message already.  Anyways...
SEND 
SEND As a final gift, this is the script used to generate xoauth2.d/dat_e
SEND and xoauth2.d/mid_e above.  It's basically same thing: I just took
SEND it from various places to mimic what the Message-ID of the first
SEND email looked like: <1751362972.auth.plain.example@e.rapidcow.org>.
SEND This doesn't hold much weight compared to the words I said above; I
SEND just think it's cool I can get it done in so few lines this time. :v
SEND 
SEND   #!/usr/bin/env perl
SEND   # make example Message ID + Date
SEND  =20
SEND   my $name =3D shift;
SEND   $name or die "usage: $0 name\n";
SEND  =20
SEND   my $derp =3D "$name.d";
SEND   -d $derp or die "! -d $derp\n";
SEND  =20
SEND   use POSIX qw(:time_h :locale_h);
SEND   use File::Spec::Functions qw(catfile);
SEND   BEGIN { *cat =3D \&catfile }
SEND  =20
SEND   setlocale (&LC_TIME, "C");
SEND   my $tem =3D time;
SEND   my $dat =3D strftime ("%a, %d %b %Y %H:%M:%S %z", localtime $tem);
SEND   my $mid =3D "<$tem.auth.xoauth2.example" . '@e.rapidcow.org' . ">";
SEND  =20
SEND   open FH, '>', cat $derp =3D> q/dat_e/ or die "open dat_e: $!\n";
SEND   print FH $dat and close FH or die "write dat_e $!\n";
SEND  =20
SEND   open FH, '>', cat $derp =3D> q/mid_e/ or die "open mid_e: $!\n";
SEND   print FH $mid and close FH or die "write mid_e: $!\n";
SEND  =20
SEND   1;
SEND 
SEND It's been my honor to talk about emails and SMTP. Until next time...
SEND (Maybe we can look at MIME? :)
SEND 
SEND ~ethan

Alright! That will be my challenge completed. Frankly I sort of cheated as... I've had months of experience doing this. But there's nothing you can't do with your bare hands, I believe! You just need a little... belief in yourself is all.

That will be the end of this series. I'll see you when I go back to Madison... I'll be minding my own business now. ;)