Part of a series on sending emails:
Index > PLAIN (@cs.wisc.edu) > XOAUTH2 (@wisc.edu)
SASL PLAIN Mechanism (RFC 4616)
Sending as @cs.wisc.edu
NOTE!!! I made a mistake when reading the CSL docs. Apparently you are supposed to use the username without "@" or the domain. (It just so happens that the SMTP server tolerates both, though strictly speaking off the docs, it shouldn't.) In the following section, pretend that @cs.wisc.edu is non-existent every time I encode the SASL PLAIN credentials (e.g. "\0emeng@cs.wisc.edu\0awwwtysm" is "\0emeng\0awwwtysm".)
Though do not omit it for Gmail because the @gmail.com is expected there! Same for Microsoft O365, which I will cover separately in the XOAUTH2 mechanism.
Let's start easy first. The CS dept is kind enough to not burden us with any OAUTH stuff: just a simple user name and password is all you need.
(Disclaimer: I have not tried this in a while, so I cannot testify if this still works. Technically they only want people to use this with the alias feature in Gmail, which in turn uses AUTH PLAIN I believe? But either way, if it works, it works. If it doesn't, I have no idea either.)
The relevant RFC is the following grammar in RFC 4616:
message = [authzid] UTF8NUL authcid UTF8NUL passwd
UTF8NUL = %x00 ; UTF-8 encoded NUL character
authzid (Authorization identity) is the identity you are
allowed to act as; I don't think I've seen it used anywhere as everyone is
pretty much only allowed to, well, act as themselves. So the only ones
that matter are authcid (Authentication identity) and
passwd (Password). These are your username and
password.
The question now is what are the username and password? Well for that, we will have to consult the CSL docs:
SMTP server: smtp-auth.cs.wisc.edu username: your CS username (bbadger not bbadger@cs.wisc.edu) password: the email authentication token generated in the previous step (Here is a direct page you can use to generate a token.) If you need to select a port number/secured connection method, use TLS wth port 587
(Note: when it says TLS, it really means STARTTLS (you can tell from the port number
too!) So remember to pass -starttls smtp to openssl s_client.
I will return to that later though.)
Also of course you need to request a token for this yeah. (And with token comes responsibility... don't reveal it to the outside world, or your email will be used for spam!)
So let's say my email is emeng@cs.wisc.edu (real) and my email token is awwwtysm (not real.) This would be my credentials:
NUL "emeng@cs.wisc.edu" NUL "awwwtysm"
^^^^^^^^^^^^^^^^^ ^^^^^^^^
authcid passwd
Note that this is really just a shorthand of saying the following sequence of characters:
NUL 'e' 'm' 'e' 'n' 'g' '@' 'c' 's'
U+00 U+65 U+6D U+65 U+6E U+67 U+40 U+63 U+73
'.' 'w' 'i' 's' 'c' '.' 'e' 'd' 'u'
U+2E U+77 U+69 U+73 U+63 U+2E U+65 U+64 U+75
NUL 'a' 'w' 'w' 'w' 't' 'y' 's' 'm'
U+00 U+61 U+77 U+77 U+77 U+74 U+79 U+73 U+6D
I emphasize "characters" and use the U+ prefix because I think it is important to think of them as such than the raw bytes: Unicode defines the characters and UTF-8 defines the encoding. Makes the whole thing easier to think about when we separate the concerns and stop limiting our perspective to 7-bit ASCII where a code point is simultaneously the code point itself, the byte, and the character. (Oh, and I am definitely giving away something. :) -- because when we pass our credentials to the SMTP server, we actually send the raw bytes.)
So continuing the example: to decode this string, since it's all ASCII we just remove U+ and take the code point itself:
NUL 'e' 'm' 'e' 'n' 'g' '@' 'c' 's'
00 65 6D 65 6E 67 40 63 73
'.' 'w' 'i' 's' 'c' '.' 'e' 'd' 'u'
2E 77 69 73 63 2E 65 64 75
NUL 'a' 'w' 'w' 'w' 't' 'y' 's' 'm'
00 61 77 77 77 74 79 73 6D
Now, the next step is to encode this string in Base64 (RFC 4422 Section 5.3 merely recommends an "encoding/escape scheme" like Base64, not require it, though in practice mail servers today all expect the credentials to be encoded in Base64 -- at least the ones I'm concerned with.) This is really the only heavy-lifting part of the authentication process, though fortunately, most programming languages come with a Base64 encoder as part of its standard library (except C, because well, it's C. :P)
Base64 is not too hard to understand either, given that you've done similar bit-wise analysis for LC-3 opcodes! Click on this <details> tab (usually displayed as a triangle to the left) for a visualization of how it's done.
Base64 -- as you can tell from 64 being the sixth power of 2 -- takes groups of 6 bits and encodes them to a Base64 alphabet: 0-25 maps to A-Z, 26-51 maps to a-z, 52-61 to 0-9. Then the last two are '+' and '/'. So if we write out all 8 bits of each byte, then group them by 6, then convert the binary string into an integer value, we can see where each Base64 digit came from:
=========== Base64 Alphabet (RFC4648) ===========
Value: 00 01 02 03 04 05 06 07 08 09 10 11 12 13 14 15
Encoding: A B C D E F G H I J K L M N O P
16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
Q R S T U V W X Y Z a b c d e f
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47
g h i j k l m n o p q r s t u v
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
w x y z 1 2 3 4 5 6 7 8 9 0 + /
nul e m
[ 0 0 ] [ 6 5 ] [ 6 D ] |octet (8bit)
e m e n g @ c s 0000 0000 0110 0101 0110 1101 |nybble(4bit)
00 65 6D 65 6E 67 40 63 73 000000 000110 010101 101101 |sextet(6bit)
. . 0 6 21 45 =VALUE
A G V t . . A G V t =ENCODING
. .
. . e n g
. . [ 6 5 ] [ 6 E ] [ 6 7 ] |oct(8)
. . 0110 0101 0110 1110 0110 0111 |nyb(4)
. . 011001 010110 111001 100111 |sex(6)
. . 25 22 57 39 =VAL(*)
Z W 5 n . Z W 5 n =ENC(*)
.
. @ c s
. [ 4 0 ] [ 6 3 ] [ 7 3 ] |oct(8)
. 0100 0000 0110 0011 0111 0011 |nyb(4)
. 010000 000110 001101 110011 |sex(6)
. 16 6 13 51 =VAL(*)
Q G N z Q G N z =ENC(*)
======================================================================
. w i
[ 2 E ] [ 7 7 ] [ 6 9 ] |oct(8)
. w i s c . e d u 0010 1110 0111 0111 0110 1001 |nyb(4)
2E 77 69 73 63 2E 65 64 75 001011 100111 011101 101001 |sex(6)
. . 11 39 29 41 =VAL(*)
L n d p . . L n d p =ENC(*)
. .
. . s c .
. . [ 7 3 ] [ 6 3 ] [ 2 E ] |oct(8)
. . 0111 0011 0110 0011 0010 1110 |nyb(4)
. . 011100 110110 001100 101110 |sex(6)
. . 28 54 12 46 =VAL(*)
c 2 M u . c 2 M u =ENC(*)
.
. e d u
. [ 6 5 ] [ 6 4 ] [ 7 5 ] |oct(8)
. 0110 0101 0110 0100 0111 0101 |nyb(4)
. 011001 010110 010001 110101 |sex(6)
. 25 22 17 53 =VAL(*)
Z W R 1 Z W R 1 =ENC(*)
======================================================================
nul a w
[ 0 0 ] [ 6 1 ] [ 7 7 ] |oct(8)
a w w w t y s m 0000 0000 0110 0001 0111 0111 |nyb(4)
00 61 77 77 77 74 79 73 6D 000000 000110 000101 110111 |sex(6)
. . 0 6 5 55 =VAL(*)
A G F 3 . . A G F 3 =ENC(*)
. .
. . w w t
. . [ 7 7 ] [ 7 7 ] [ 7 4 ] |oct(8)
. . 0111 0111 0111 0111 0111 0100 |nyb(4)
. . 011101 110111 011101 110100 |sex(6)
. . 29 55 29 52 =VAL(*)
d 3 d 0 . d 3 d 0 =ENC(*)
.
. y s m
. [ 7 9 ] [ 7 3 ] [ 6 D ] |oct(8)
. 0111 1001 0111 0011 0110 1101 |nyb(4)
. 011110 010111 001101 101101 |sex(6)
. 30 23 13 45 =VAL(*)
e X N t e X N t =ENC(*)
======================================================================
(End of <details>! You may resume reading...)
Here's how you can perform the two steps in a few programming languages in decreasing levels of popularity:
// Java (StandardCharset added in 1.6, others idk)
import java.util.Base64;
import static java.nio.charset.StandardCharsets.UTF_8;
String stringCredentials = "\0emeng@cs.wisc.edu\0awwwtysm";
byte[] binaryCredentials = stringCredentials.getBytes(UTF_8);
String asciiCredentials = Base64.getEncoder()
.encodeToString(binaryCredentials);
System.out.println(
asciiCredentials
); // AGVtZW5nQGNzLndpc2MuZWR1AGF3d3d0eXNt
# Python 3 (maybe 2 as well, I don't know)
import base64 # see also: binascii.b2a_base64
str_creds = '\0emeng@cs.wisc.edu\0awwwtysm'
bin_creds = str_creds.encode('utf-8', 'strict')
asc_creds = base64.b64encode(bin_creds)
print(asc_creds) # b'AGVtZW5nQGNzLndpc2MuZWR1AGF3d3d0eXNt'
# Perl 5 (no non-core dependencies since 5.7+)
use Encode; # exports encode
use MIME::Base64; # exports encode_base64
my $str_creds = "\0emeng\@cs.wisc.edu\0awwwtysm";
my $bin_creds = encode 'UTF-8' => $str_creds;
# Setting EOL to "" is important! Unlike MIME bodies,
# I'm not sure if SASL auth ever expects multiple lines.
my $asc_creds = encode_base64 $bin_creds, "";
print $asc_creds, "\n"; # AGVtZW5nQGNzLndpc2MuZWR1AGF3d3d0eXNt
If you have a Unix shell (that includes Bash, of course), you can do it in two steps:
- Use printf(1)
to create the credentials (note that you do not type the "
$"; that just means you shell prompt):
Pipe it to any binary dump program to check if the output does not contain a line feed. I prefer hex dumps, and here are a few common ways to produce a hex dump:$ printf '\0%s\0%s' 'emeng@cs.wisc.edu' 'awwwtysm'
Here is a bad example. Pay attention to the extraneous added by not passing a# # Text in bold are commands you should type into a # Bourne shell. They are a bit obnoxiously long, but # the good thing is these are all REAL commands with # REAL outputs! I will stick to xxd(1) and Bash # since it's the nicest to use and what I have. I # try to show how to do something similar with Unix # shells in general though. # # Note that I set the column width to 9 bytes because # it's nicer for comparing against the Base64 string # later. Usually `xxd' alone is enough. # $ printf '\0%s\0%s' 'emeng@cs.wisc.edu' 'awwwtysm' | xxd -c9 00000000: 0065 6d65 6e67 4063 73 .emeng@cs 00000009: 2e77 6973 632e 6564 75 .wisc.edu 00000012: 0061 7777 7774 7973 6d .awwwtysm # # If you do not have xxd(1) installed, try # hexdump(1) or od(1), though you'd # need slightly more flags to make it look "nice". # # Note: `hexdump -C' is usually enough. # (I only passed a format because -C is too wide) # # Also note that you don't need explicit line continua- # tion when your command ends with a "|", "&&", or "||" # (It is quite the opposite for "&", but I haven't # figured out that as much....) # $ printf '\0%s\0%s' 'emeng@cs.wisc.edu' 'awwwtysm' | hexdump \ -e '"%08.8_Ax\n"' \ -e '"%08.8_ax " 9/1 "%02x "' \ -e '" " "|" "%_p"' \ -e '"|\n"' 00000000 00 65 6d 65 6e 67 40 63 73 |.emeng@cs| 00000009 2e 77 69 73 63 2e 65 64 75 |.wisc.edu| 00000012 00 61 77 77 77 74 79 73 6d |.awwwtysm| 0000001b # # od(1), in principle, is an octal dump. It is therefore # under no obligation to use hex addresses or hex numbers # unless we explicitly tell it to use so with -Ax and -tx. # The additional switches are GNU extensions to make things # look prettier; remove `z' (comparing the output to that # of `-tc' makes up for it) and `-w9' (a wider screen makes # up for it) for it to work on BSD Unix. # $ printf '\0%s\0%s' 'emeng@cs.wisc.edu' 'awwwtysm' | od -Ax -tx1z -w9 000000 00 65 6d 65 6e 67 40 63 73 >.emeng@cs< 000009 2e 77 69 73 63 2e 65 64 75 >.wisc.edu< 000012 00 61 77 77 77 74 79 73 6d >.awwwtysm< 00001b-nto echo:
The single quotes tell the shell to interpret everything verbatim except the single quote itself (and the null byte, obviously). Chances are, neither will your email nor your password contain meta-characters like these.# WRONG: There is a \x0a (line feed/newline) at the end { AUTHCID='emeng@cs.wisc.edu' PASSWD='awwwtysm' echo -e "\0${AUTHCID}\0${PASSWD}" } | xxd -c9 00000000: 0065 6d65 6e67 4063 73 .emeng@cs 00000009: 2e77 6973 632e 6564 75 .wisc.edu 00000012: 0061 7777 7774 7973 6d .awwwtysm 0000001b: 0a . # OK: (though still I don't trust echo; use printf(1) please :) { AUTHCID='emeng@cs.wisc.edu' PASSWD='awwwtysm' echo -n -e "\0$AUTHCID\0$PASSWD" } | xxd -c9 00000000: 0065 6d65 6e67 4063 73 .emeng@cs 00000009: 2e77 6973 632e 6564 75 .wisc.edu 00000012: 0061 7777 7774 7973 6d .awwwtysm # Also OK: (if "\n" occurs nowhere in authcid or passwd) { AUTHCID='emeng@cs.wisc.edu' PASSWD='awwwtysm' echo -e "\0$AUTHCID\0$PASSWD" } | tr -d '\n' | xxd -c9 00000000: 0065 6d65 6e67 4063 73 .emeng@cs 00000009: 2e77 6973 632e 6564 75 .wisc.edu 00000012: 0061 7777 7774 7973 6d .awwwtysm - Pipe it to base64(1),
or encode it from stdin yourself using your favorite scripting
language:
# # base64(1) usually wraps at a certain number of characters, # (usually 76 columns) which again (as I have remarked for # Perl's MIME::Base64) is desirable for email bodies, but un- # desirable for SASL credentials. Here we use tr(1) to delete # them, which may seem excessive in this case for such a short # password, but will definitely prove to be so for the XOAUTH2 # mechanism. (If you find the lack of trailing newline ugly, # put an echo command at the end, like this:) # $ printf '\0%s\0%s' 'emeng@cs.wisc.edu' 'awwwtysm' | base64 | tr -d '\n' && echo AGVtZW5nQGNzLndpc2MuZWR1AGF3d3d0eXNt # # Note the lack of indentation within the Python command (-c). # I should not have to explain why this is needed. You know # what happens when you mess with Python's indent.... # $ printf '\0%s\0%s' 'emeng@cs.wisc.edu' 'awwwtysm' | python3 -c ' import base64 import sys bin_cred = sys.stdin.buffer.read() asc_cred = base64.b64encode(bin_cred) sys.stdout.buffer.write(asc_cred) print() ' AGVtZW5nQGNzLndpc2MuZWR1AGF3d3d0eXNt # As a true one-liner... $ printf '\0%s\0%s' 'emeng@cs.wisc.edu' 'awwwtysm' | python3 -c 'from sys import *; import base64; stdout.buffer.write(base64.b64encode(stdin.buffer.read())); print(flush=True)' AGVtZW5nQGNzLndpc2MuZWR1AGF3d3d0eXNt # # With Perl there are several ways to do it: we can slurp # in the whole thing like we did with stdin.buffer.read(): # $ printf '\0%s\0%s' 'emeng@cs.wisc.edu' 'awwwtysm' | perl -MMIME::Base64 -0777 -nE 'say encode_base64 $_, ""' AGVtZW5nQGNzLndpc2MuZWR1AGF3d3d0eXNt # # Or we can read in chunks of 3 bytes, only appending the # newline at the very end... (of course, we can set $/ to # any multiple of 3. 3 is an absurdly small, and likely # inefficient chunk size. Something closer to a kilobyte # might make more sense -- but that is assuming you would # be using Base64 for anything other than encoding SASL...) # $ printf '\0%s\0%s' 'emeng@cs.wisc.edu' 'awwwtysm' | perl -MMIME::Base64 -E '{ local $/ = \3; print encode_base64 $_, "" while <STDIN> } say ""' AGVtZW5nQGNzLndpc2MuZWR1AGF3d3d0eXNt
I have to hold back on the entire Unix shell thing because the Unix pipe one of the most butchered Linux/Bash topics in CS400. I have a collection of resources you can learn from, but otherwise it's up to you to choose between: The safe and sturdy Java...
/* Details in the file or Git repository on GitHub... */
public class PlainSaslSmtpAuthExample {
private String myUsername = /* SASL PLAIN authcid */;
private String myPassword = /* SASL PLAIN passwd */;
public void run() {
String credentials = "\0" + myUsername + "\0" + myPassword;
byte[] binaryCredentials = credentials.getBytes(StandardCharsets.UTF_8);
String base64Credentials = Base64.getEncoder().encodeToString(binaryCredentials);
System.out.println("AUTH PLAIN " + base64Credentials);
}
}
... or everything else (I'm not turning those into a program though; one is enough. I leave you to the rest, sorry. :P)
CHALLENGE: send an email with SASL PLAIN!
Okay. You've done all the reading. There is not much else to do now, except... actually sending an email? :)
Here's the good news: this time I figured out a good place for you to practice. Rather than sending to me... send it to <~rapidcow/wiscmail-inbox@lists.sr.ht> (note the plural "s" in lists)! I'm going to do it once and, if you can, I want you to be try the same... (except for the first step, of course. You don't have to send the same message as me.)
Prepare your message. Use a text editor, and pay attention to your character-per-line limit! A good Netizen keeps it under 76 characters per line (78 characters minus the CRLF is RFC 5322's recommendation, the two more characters are for when somebody has to quote your (which happens a lot and dates back to the days of Usenet... (just... keep it under 76 characters, ok? Or at least 132 would be nice...)))
I haven't talked much about MIME, but lucky for us, Sourcehut wants none of that fancy HTML stuff! So a plain text email is all we need to prepare. Here's what I'll send...
Message-ID: <1751362972.auth.plain.example@e.rapidcow.org> Date: Tue, 01 Jul 2025 17:42:52 +0800 From: Ethan Meng <emeng@cs.wisc.edu> To: ~rapidcow/wiscmail-inbox@lists.sr.ht Subject: Sending with AUTH PLAIN MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: 7bit ============ End of headers! Start of content! ============ (Type whatever you want. Use AI if you like, but please don't just copy me. That would be embarrassing.) To keep things simple, Content-Transfer-Encoding is 7bit. 64 characters is this long: ================================================================ 76 characters is this long: ============================================================================Replace everything that is in bold with your own stuff. (You can keep the subject though.)
Next step for me would be to get my real password (to replace what I had before with, well... 'awwwtysm'). Now, as a totally non-commercial plug, I use the standard Unix password manager, and this is how I get my password:
$ pass show Token/cs.wisc.edu | head -c5 && echo Yz8h8[This is my real token! So only 5 bytes shown.]
Now I can use the handy Java code I just happened to write up a while ago to encode the password: (you can get the same in case you skipped to this part! Jump back to the example code.)
$ javac -d out src/PlainSaslSmtpAuthExample.java $ java -cp out PlainSaslSmtpAuthExample Username: emeng@cs.wisc.edu Password: [noecho]Yz8h8... AGVtZW5nQGNzLndpc2MuZWR1AFl6OGg4...Or just use the good ole shell... (what I usually do anyways):
$ printf '\0%s\0%s' 'emeng@cs.wisc.edu' "$(pass show Token/cs.wisc.edu)" | base64 | tr -d '\n' && echo AGVtZW5nQGNzLndpc2MuZWR1AFl6OGg4...You will notice that I also truncated the Base64 string because we should remember that Base64 is an encoding, which means that unlike cryptographic signatures that just certifies some data, Base64 is a two-way street that allows for recovery of the data. Basically giving away your Base64-encoded password would be handing over the real stuff, so be mindful of that. (If you are paranoid like me, remove by multiples of 3, then decode it to see if you've removed enough of the sensitive stuff.
One more thing before we do the deed! Prepare your MAIL FROM/RCPT TO addresses: generally you just list yourself and everyone you will send to. (Note that the way blind carbon copy (Bcc) works is that, your email client (MUA) -- behind the scene -- strips the Bcc field but remembers to put it in RCPT TO. So yes, it is very possible that your RCPT TO recipients differ from who you mail claims to deliver to. And it certainly is possible that you send more than who you actually deliver to; the To/Cc fields are more informative than they are authoritive like DKIM/DMARC. But anyways, Still, just be nice and make them as consistent as possible. :)
For me I only have one recipient, the mailing list. So this should suffice:
MAIL FROM:<emeng@cs.wisc.edu> RCPT TO:<~rapidcow/wiscmail-inbox@lists.sr.ht>
Note the angled brackets
<>: don't forget to add those as I don't think they are optional. (Again, replace my email with your own!)
Once you've done all that, you are ready to send an email!
First connect to the SMTP server (again, don't type the "$",
though you can also tell from the fact that it isn't in bold text.
(Bold text traditionally means user input, FYI. (Also, if you are
using the Windows terminal, drop the -crlf.))):
$ openssl s_client -crlf -starttls smtp -nocommands -connect smtp-auth.cs.wisc.edu:587
You should see a bunch of stuff happen:
Connecting to 128.105.6.3
CONNECTED(00000003)
depth=2 C=US, ST=New Jersey, L=Jersey City, O=The USERTRUST Network, CN=USERTrust RSA Certification Authority
verify return:1
depth=1 C=US, O=Internet2, CN=InCommon RSA Server CA 2
verify return:1
depth=0 C=US, ST=Wisconsin, O=University of Wisconsin-Madison, CN=cs.wisc.edu
verify return:1
---
Certificate chain
0 s:C=US, ST=Wisconsin, O=University of Wisconsin-Madison, CN=cs.wisc.edu
[omitted...]
1 s:C=US, O=Internet2, CN=InCommon RSA Server CA 2
[omitted...]
2 s:C=US, ST=New Jersey, L=Jersey City, O=The USERTRUST Network, CN=USERTrust RSA Certification Authority
[omitted...]
3 s:C=GB, ST=Greater Manchester, L=Salford, O=Comodo CA Limited, CN=AAA Certificate Services
---
Server certificate
-----BEGIN CERTIFICATE-----
[very long stuff]
-----END CERTIFICATE-----
subject=C=US, ST=Wisconsin, O=University of Wisconsin-Madison, CN=cs.wisc.edu
issuer=C=US, O=Internet2, CN=InCommon RSA Server CA 2
---
No client certificate CA names sent
Peer signing digest: SHA256
Peer signature type: rsa_pss_rsae_sha256
Peer Temp Key: X25519, 253 bits
---
SSL handshake has read 6680 bytes and written 1667 bytes
Verification: OK
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Protocol: TLSv1.3
Server public key is 2048 bit
This TLS version forbids renegotiation.
Compression: NONE
Expansion: NONE
No ALPN negotiated
Early data was not sent
Verify return code: 0 (ok)
---
250 CHUNKING
---
Post-Handshake New Session Ticket arrived:
SSL-Session:
Protocol : TLSv1.3
Cipher : TLS_AES_256_GCM_SHA384
[blah blah blah this goes on for a while]
---
read R BLOCK
... and that's what we are looking for! OpenSSL upgraded our connection with STARTTLS, the server is reset back to a state as if it never talked to us yet. So now is the time to re-initiate our session with an EHLO:
EHLO localhost
250-smtp-auth-01.cs.wisc.edu
250-PIPELINING
250-SIZE 24576000
250-VRFY
250-ETRN
250-AUTH PLAIN LOGIN
250-AUTH=PLAIN LOGIN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250-DSN
250 CHUNKING
(I have no idea what you should put as the host after HELO/EHLO; just put whatever looks right to you I guess. :/)
Now, pay attention to the authentication mechanism: "AUTH PLAIN LOGIN". That means this server supports SASL PLAIN. Now this is something you already know because -- well -- I told you that. But for a mail client to be know that this is a valid mechanism it would have to look at these lines. (These lines are known as capabilities, though because we initiated EHLO, they are actually extended capabilities. They are usually specified somewhere in the RFCs: for example, 8BITMIME, the extension which allows us to send messages with an 8-bit body using a character set that extends ASCII in a reasonably compatible way (UTF-8 being the perfect candidate for this), is specified in RFC 6152.)
Now for the important step: authentication. (This is something you just usually have to know, I don't know if you can tell from the extensions advertised by the server.) Remember the SASL PLAIN and Base64 I spent many paragraphs explaining? Well -- now you can copy it and just paste it in!
AUTH PLAIN
334
AGVtZW5nQGNzLndpc2MuZWR1AFl6OGg4...
235 2.7.0 Authentication successful
If it says unsuccessful, decode your Base64 and view it with either xxd(1), hexdump(1), or od(1) to see what went wrong. REMEMBER: The password has no newline (line feed)!
Okay. The hard part is over. Now we prepare for mail transaction using three steps. The first two are MAIL FROM and RCPT TO, which you should have ready with you:
MAIL FROM:<emeng@cs.wisc.edu>
RCPT TO:<~rapidcow/wiscmail-inbox@lists.sr.ht>
250 2.1.0 Ok
250 2.1.5 Ok
Note that if you send these two lines with considerable delay (i.e. typing by hand instead of pasting), you may see the "250 Ok" line appear right after you type. Either way it's normal, as long as you get as many responses as the commands you typed in.
The third and final step is to issue the DATA command:
DATA
354 End data with <CR><LF>.<CR><LF>
And type your message! End with a period "." on its own line.
If everything worked out, you should see your message appear in the mailing list. :)
Bonus: Sending as @gmail.com
It's almost the same thing except:
- You replace smtp-auth.cs.wisc.edu with smtp.gmail.com;
- You either keep the same port or switch to 465 (Gmail supports either);
- Instead of the Gmail token you get from apps.cs.wisc.edu, you request an app password from Google. Give it a name you will remember, like "SMTP Sendmail", "IMAP/SMTP Access", "Email token" (though I think email is the only thing the app password is good for... I have no idea. Google never tells us :/) Because this is the same place you will return to when you want to revoke the token (either because you don't send/receive emails anymore, or worse, the token gets compromised.).
- And of course, you replace all your @cs.wisc.edu with your @gmail.com.
If you use port 587 you do the same with -starttls smtp:
$ openssl s_client -crlf -nocommands -starttls smtp smtp.gmail.com:587
With port 465 you just drop the -starttls smtp, the same
way you connect to HTTPS. (This is sometimes called SMTPS for that reason,
by the way, in case you see that in a mail client configuration!)
$ openssl s_client -crlf -nocommands smtp.gmail.com:465
Then once you log in you do the same thing: EHLO yourdomain,
AUTH PLAIN followed by Base64-encoded credentials, then
MAIL FROM, RCPT TO, and DATA
followed by a period on its own line. (On Windows, drop the -crlf
in all cases.)
Bonus2: Sending both with Perl Net::SMTP
The example script I wrote for Piazza can be used to do all of the above with less hassle! Of course, this isn't instantly magic: you can still see, down to each SMTP command, what it does to get your mail queued. The benefit is that it watches the error codes for us in case anything goes wrong.
The way you use it is pretty simple. First, retrieve the Perl file either by cloning from the queue.pl repository using either one of the following remote URIs:
$ git clone https://github.com/eyzmeng/queue.pl.git
$ git clone git@github.com:eyzmeng/queue.pl.git
or get it from the
Gist.
If you decide to clone the repository,
the default branch, CSLab, is suitable for sending as @cs.wisc.edu,
but you can checkout the Gmail branch for sending as @gmail.com.
In both cases though, you should have GMAILqueue.patch, which
you can use to create the Gmail version by running
"patch -p1 < GMAILqueue.patch". Revert back to the
CS version using "patch -p1 -R < GMAILqueue.patch".
Then install Perl 5 (on Mac and Linux, odds are it is already installed.
On Windows I can vouch for Strawberry Perl. Having a Cygwin/MinGW environment like MSYS2 is also an option; and I shouldn't have
to explain to you why WSL would work for this too.)
Then depending on where you are, install the dependencies:
most importantly there will be Net::SMTP, but for SSL connection
you need IO::Socket::SSL and Net::SSLeay (the latter depends on the former),
which aren't easy to say the least... but if you do get it installed,
then good news!
The script also optionally depends on the UUID module to generate a Message-ID. To use it, find the following excerpt and un-comment the Message-ID line (underlined below):
my %headers = (
# 'Date' => rfc2822_date (),
# 'Message-ID' => '<' . uuidgen() . '@csl.example.org>',
'Subject' => 'A Test Email with Perl',
);
If you do, you will need to install with cpan UUID (maybe built-in) or cpanm UUID (look for cpanminus.)
Now the rest is just configuring stuff for yourself! I'll do one
example with the @cs.wisc.edu email with the following configuration
(the changed parts are in bold):
my @message = split /^/, <<'EOM';
You can watch how this email was sent here!
<https://asciinema.org/a/727450>
EOM
my %headers = (
# These were uncommented. Of course, I had to install UUID...
'Date' => rfc2822_date (),
'Message-ID' => '<' . uuidgen() . '@csl.example.org>',
'Subject' => 'Re: Sending from port 25',
);
# Message context/reference headers:
# https://cr.yp.to/immhf/thread.html#references
my @context = ( # most recent last
'<1752296717.43109.port25@example.rapidcow.org>',
# If there are more messages in this thread, they are appended below.
);
$headers{'In-Reply-To'} = join "\n ", @context;
$headers{'References'} = $context[$#context];
push @message, split /^/, <<'EOM';
///` -.-.-.-.-.-.-.-.-.-.-.- '\\\
||| : insert cool signature : |||
\\\. -.-.-.-.-.-.-.-.-.-.-.- .///
EOM
my $user = 'emeng';
my $name = 'Ethan Meng';
# Again, instead of using pass(1), you can just hard-code
# your password here... but *I* do use pass, so I'll use it!
my $pass = `pass show Token/cs.wisc.edu`
or die "pass printed nothing; code ", $? >> 8;
($pass) = $pass =~ /^(.+)$/m; # keep the first line
my $to = '~rapidcow/wiscmail-inbox@lists.sr.ht';
my $port = 587;
my $bits = 7;
Then I just run perl queue.pl and pray for a miracle...
$ ./queue.pl
SMTP commands:
CONNECT smtp-auth.cs.wisc.edu:587
AUTH PLAIN AGVtZW5nAFl6[SECRET!]
MAIL FROM:<emeng@cs.wisc.edu>
RCPT TO:<~rapidcow/wiscmail-inbox@lists.sr.ht>
RCPT TO:<emeng@cs.wisc.edu>
Email headers:
From: "Ethan Meng" <emeng@cs.wisc.edu>
Content-Transfer-Encoding: 7bit
MIME-Version: 1.0
In-Reply-To: <1752296717.43109.port25@example.rapidcow.org>
To: ~rapidcow/wiscmail-inbox@lists.sr.ht
Content-Type: text/plain; charset="us-ascii"
Subject: Re: Sending from port 25
Date: Sat, 12 Jul 2025 22:26:32 +0800
Message-ID: <15164018-733c-4869-a7ad-cc13f61b8067@csl.example.org>
References: <1752296717.43109.port25@example.rapidcow.org>
Okay... this looks alright to me. So I press y followed by CR (i.e. the Enter key).
Does this look ok? (y/[n]) y
Connecting to smtp-auth.cs.wisc.edu:587...
Connected to smtp-auth.cs.wisc.edu.
Negotiating STARTTLS...
Authenticating as emeng...
MAIL FROM:<emeng@cs.wisc.edu>
RCPT TO:<~rapidcow/wiscmail-inbox@lists.sr.ht> <emeng@cs.wisc.edu>
DATA BEGIN
SEND 'From: "Ethan Meng" <emeng@cs.wisc.edu>'
SEND 'Content-Transfer-Encoding: 7bit'
SEND 'MIME-Version: 1.0'
SEND 'In-Reply-To: <1752296717.43109.port25@example.rapidcow.org>'
SEND 'To: ~rapidcow/wiscmail-inbox@lists.sr.ht'
SEND 'Content-Type: text/plain; charset="us-ascii"'
SEND 'Subject: Re: Sending from port 25'
SEND 'Date: Sat, 12 Jul 2025 22:26:32 +0800'
SEND 'Message-ID: <15164018-733c-4869-a7ad-cc13f61b8067@csl.example.org>'
SEND 'References: <1752296717.43109.port25@example.rapidcow.org>'
SEND ''
SEND 'You can watch how this email was sent here!'
SEND '<https://asciinema.org/a/727450>'
SEND ''
SEND '///` -.-.-.-.-.-.-.-.-.-.-.- \'\\\\\\'
SEND '||| : insert cool signature : |||'
SEND '\\\\\\. -.-.-.-.-.-.-.-.-.-.-.- .///'
DATA END
And now it appears here as a reply to my previous raw SMTP example!