The "Simple" Mail Transfer Protocol (SMTP) is not that simple. To reduce the complexity go between programs were developed such as ssmtp, msmtp and swaks.
Unfortunately, with these programs, comes limitations. For example, ssmpt only support two authentication methods: LOGIN and CRAM-MD5.
Proton's smtp-submission requires that the authentication method be PLAIN.
Furthermore, ssmtp has not been maintained since 2019. Debian suggests using msmtp as its replacement. However, I found it to be more complex than swaks.
However, because of spammer, the authentication for logging into one of these servers via SMPT has become complex.
Originally, I used Swaks (Swiss Army Knife for smpt). Later, I learned that I could do the same with cURL.
Extended SMPT (ESMTP) expands the original protocol to include email attachments, TLS, and other capabilities. Today, almost all email clients and email services use ESMTP, not basic SMTP.
SMTP and ESMPT use port 587.
Some optional SMTP commands are:
According to Reference [5] Proton Mail uses port 1025, while most SMTP servers use port 587. My experience has been that port 587 works fine with Proton Mail.
The step-by-step procedure here follows Reference [4].
In this example:
You can confirm the SMTP server is active and responding with the command:
openssl s_client -nocommands -starttls smtp -connect smtp.protonmail.ch:587
Connecting to 176.119.200.135
CONNECTED(00000003)
depth=3 C=US, O=Internet Security Research Group, CN=ISRG Root X1
verify return:1
depth=2 C=US, O=ISRG, CN=Root YR
verify return:1
depth=1 C=US, O=Let's Encrypt, CN=YR1
verify return:1
depth=0 CN=protonmail.com
verify return:1
---
Certificate chain
0 s:CN=protonmail.com
i:C=US, O=Let's Encrypt, CN=YR1
a:PKEY: RSA, 4096 (bit); sigalg: sha256WithRSAEncryption
v:NotBefore: Sep 7 13:14:28 2026 GMT; NotAfter: Dec 6 13:14:27 2026 GMT
1 s:C=US, O=Let's Encrypt, CN=YR1
i:C=US, O=ISRG, CN=Root YR
a:PKEY: RSA, 2048 (bit); sigalg: sha256WithRSAEncryption
v:NotBefore: Sep 3 00:00:00 2025 GMT; NotAfter: Sep 2 23:59:59 2028 GMT
2 s:C=US, O=ISRG, CN=Root YR
i:C=US, O=Internet Security Research Group, CN=ISRG Root X1
a:PKEY: RSA, 4096 (bit); sigalg: sha256WithRSAEncryption
v:NotBefore: May 13 00:00:00 2026 GMT; NotAfter: Sep 2 23:59:59 2032 GMT
---
Server certificate
-----BEGIN CERTIFICATE-----
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
lb1LCMGQzAZJUS9hPJZfVx4xb/jEYno=
-----END CERTIFICATE-----
subject=CN=protonmail.com
issuer=C=US, O=Let's Encrypt, CN=YR1
---
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 5418 bytes and written 1673 bytes
Verification: OK
---
New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
Protocol: TLSv1.3
Server public key is 4096 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
Session-ID: B22183FCD9B6B769470246C9FEE9E4EA76CBF3551A8590CD5AD13585FE5FBB81
Session-ID-ctx:
Resumption PSK: E75D02AC63E74EBA5ED29C34C8D58AB4E2ED4C633298F18BEC426FB0559EE52504327A292EED49E3CD1D1AD1240B4701
PSK identity: None
PSK identity hint: None
SRP username: None
TLS session ticket lifetime hint: 7200 (seconds)
TLS session ticket:
0000 - 4d b8 95 90 c8 0b af 54-8a 87 33 7b 03 17 a5 25 M......T..3{...%
0010 - db 25 62 c7 1b 8d e0 19-d6 31 c6 ba f9 8d 43 3f .%b......1....C?
0020 - 1a c4 de 3d cb 4b 91 66-79 50 a3 c4 54 22 bb f8 ...=.K.fyP..T"..
0030 - 23 01 d4 c3 9e de 65 12-fb 6e a3 92 b5 79 c8 01 #.....e..n...y..
0040 - 74 e0 14 e4 67 2f a7 bc-dd c2 97 f4 79 3a 7b 22 t...g/......y:{"
0050 - 27 7a 2e 96 4c a7 69 d9-75 8e 92 c4 40 59 60 b1 'z..L.i.u...@Y`.
0060 - 16 7f c0 3e 78 ff 13 40-84 e1 ce 98 1f 93 de 7c ...>x..@.......|
0070 - 69 d3 c6 78 b8 85 34 9f-d7 ee 78 42 14 97 0f 39 i..x..4...xB...9
0080 - f4 c7 e7 af 40 73 97 fa-c0 6e 3e 50 3f 8b db f3 ....@s...n>P?...
0090 - 1d e0 ea e2 d4 d5 1a 02-4c fd e1 56 fd c1 16 a7 ........L..V....
00a0 - 5b 3f 25 f0 1c 7a 7f cd-d9 00 ca 50 67 d7 d4 57 [?%..z.....Pg..W
00b0 - d7 d4 b7 8c 82 0f ee da-2e 0e c6 f6 a1 a8 99 03 ................
00c0 - 71 ae d6 e1 27 72 5b 4d-22 c6 17 52 06 3b 54 c9 q...'r[M"..R.;T.
Start Time: 1790427570
Timeout : 7200 (sec)
Verify return code: 0 (ok)
Extended master secret: no
Max Early Data: 0
---
read R BLOCK
Send EHLO:
EHLO 127.0.0.1
You should receive a response similar to:
250-mailsubzur1002.protonmail.ch
250-PIPELINING
250-SIZE 36480000
250-AUTH PLAIN LOGIN
250-ENHANCEDSTATUSCODES
250-8BITMIME
250 CHUNKING
Send the SMTP server your authentication:
AUTH PLAIN AHJheUBmcmFuY28ubXMAWE00MlhGUjZHMlFDNDlRTA==
You should receive a response similar to:
235 2.7.0 Authentication successful
Send the SMTP server the Mail From:
MAIL FROM: <ray@franco.ms>
You should receive a response similar to:
250 2.1.0 OK
Send the SMPT server the recipient's email address:
RCPT TO: <ray@rayfranco.com>
You should receive the 250 OK response:
250 2.1.5 OK
To send your message:
DATA
Wait for the server to response back:
354 Send message content; end with <CRLF>.<CRLF>
Send your message:
From: ray@franco.ms
To: ray@rayfranco.com
Subject: Manual - Server Status
The Server is UP
.
Wait for the 250 OK.
250 2.0.0 OK: queued as 4hsYj10WLCz1DFG3
End the communications:
QUIT
You should receive a response similar to:
221 2.0.0 Bye
closed
I had two minor hiccups implementing the step-by-step example:
swaks stands for: Swiss army knife for smtp
Swaks was first released in 2003 [1]. As of September 2026, the latest version is 20240103.0, which was released in 2024. Swaks was written and maintained by an individual: John Jetmore. Swaks is in the Debian and Raspberry Pi repositories. It is not installed by default. To install it:
sudo apt install swaks
In the following examples:
#!/bin/bash
#----- function send_emai -------
send_email () {
MESSAGE="From: ray@franco.ms\nTo: ray@rayfranco.com\nSubject: Server Status via SWAKS\n\nThe Server is $1 - $(date)\n"
swaks \
--from ray@franco.ms \
--to ray@rayfranco.com \
--server smtp.protonmail.ch \
--port 587 \
--auth PLAIN \
--tls \
--auth-user 'ray@franco.ms' \
--auth-password 'My_Redacted_SMTP_Password' \
--ehlo 127.0.0.1 \
--data "$MESSAGE"
}
#-------- Main ----------
send_email UP
SWAKS, outputs SMTP communications by default. There is no need for the verbose option. The output is:
=== Trying smtp.protonmail.ch:587... === Connected to smtp.protonmail.ch. <- 220 mailsubosl1001.protonmail.ch ESMTP Postfix -> EHLO 127.0.0.1 <- 250-mailsubosl1001.protonmail.ch <- 250-PIPELINING <- 250-SIZE 36480000 <- 250-STARTTLS <- 250-ENHANCEDSTATUSCODES <- 250-8BITMIME <- 250 CHUNKING -> STARTTLS <- 220 2.0.0 Ready to start TLS === TLS started with cipher TLSv1.3:TLS_AES_256_GCM_SHA384:256 === TLS client certificate not requested and not sent === TLS no client certificate set === TLS peer[0] subject=[/CN=protonmail.com] === commonName=[protonmail.com], subjectAltName=[DNS:*.pm.me, DNS:*.protonmail.ch, DNS:*.protonmail.com, DNS:*.protonvpn.ch, DNS:*.protonvpn.com, DNS:protonmail.com] notAfter=[2026-12-06T13:14:27Z] === TLS peer[1] subject=[/C=US/O=Let's Encrypt/CN=YR1] === commonName=[YR1], subjectAltName=[] notAfter=[2028-09-02T23:59:59Z] === TLS peer[2] subject=[/C=US/O=ISRG/CN=Root YR] === commonName=[Root YR], subjectAltName=[] notAfter=[2032-09-02T23:59:59Z] === TLS peer certificate passed CA verification, passed host verification (using host smtp.protonmail.ch to verify) ~> EHLO 127.0.0.1 <~ 250-mailsubosl1001.protonmail.ch <~ 250-PIPELINING <~ 250-SIZE 36480000 <~ 250-AUTH PLAIN LOGIN <~ 250-ENHANCEDSTATUSCODES <~ 250-8BITMIME <~ 250 CHUNKING ~> AUTH PLAIN AHJheUBmcmFuY28ubXMAWE00MlhGUjZHMlFDNDlRTA== <~ 235 2.7.0 Authentication successful ~> MAIL FROM:<~ 250 2.1.0 Ok ~> RCPT TO: <~ 250 2.1.5 Ok ~> DATA <~ 354 End data with . ~> From: ray@franco.ms ~> To: ray@rayfranco.com ~> Subject: Server Status via SWAKS ~> ~> The Server is UP - Thu Sep 24 08:31:03 AM CDT 2026 ~> ~> . <~ 250 2.0.0 Ok: queued as 4hrF8p4RNCz1DDLL ~> QUIT <~ 221 2.0.0 Bye === Connection closed with remote host.
The "--from ..." and "--to ..." options are what the SMTP server uses to send and recieve emails.
The "From: ..." and "To: ..." in the data or message are what the recipient's client uses when it displays the email's sender and recipient. If you do not include the "From: ..." and "To: ..." in the data or message, then the recipient's email client will use value in the "--from ..." option as the sender, but for the recipient, it will say "undisclosed".
In lieu of putting the headers and email body in the one message, SWAKS allows both header and body options. The code below does the same as the previous SWAKS code:
#!/bin/bash
#----- function send_emai -------
send_email () {
SUBJECT="Sever Status via SWAKS 2"
BODY="The Server is $1 - $(date)\n"
swaks \
--from ray@franco.ms \
--to ray@rayfranco.com \
--server smtp.protonmail.ch \
--port 587 \
--auth PLAIN \
--tls \
--auth-user 'ray@franco.ms' \
--auth-password 'My_Redacted_SMTP_Password' \
--ehlo 127.0.0.1 \
--header "Subject: $SUBJECT" \
--body "$BODY"
}
#-------- Main ----------
send_email UP
There is no "From: ..." or "To: ..." in the header or body. In the case, the recipient's client will use the values in the options for BOTH the sender and recipient.
Where I ran into problems with SWAKS was trying to put today's date in the subject heading. I tried, $(date), $(date "+%D"), $(date "+%Y/%m/%d), $(date "+%Y-%m-%d"), $(date "+%Y %m %d"). It gave me a error, and it would not send the email. Finally, I tried $(date "+%Y_%m_%d"), and this worked. In the documentation, there is comment about not using a dash in the SWAKs configuration file, but that is all I found. This is not well documented.
As long as the email headers and body that you wanted to send is in a file, cURL is easy to use. For example:
#!/bin/bash
curl --ssl smtp://smtp.protonmail.ch:587 \
--mail-from ray@franco.ms \
--mail-rcpt ray@rayfranco.com \
--user 'ray@franco.ms:My_Redactecd_SMTP_Password' \
--upload-file MESSAGE.txt
where MESSAGE.txt is:
From: ray@franco.ms
To: ray@rayfranco.com
Subject: Server Status
The server is up.
Notes:
The problem is if you do not use the --upload-file <file_name> option (or its short form -T <file_name>), then cURL send a VFRY (verify) command to the mail service to verify that the mailbox is valid (exist). However, for security reasons Proton Mail disables VFRY.
Thus, you cannot use the --data option, and you must use the --upload-file <file_name> option. However, when you use the --upload-file <file_name> with "Process Substitution" instead of a actual file, curl does not know what the use for HELO or EHLO, so the SMTP server gives the error: "Helo command rejected: Invalid name"
The solution, for the Helo error, is to include the client's Helo (or extended helo -ehlo) with the url. That is use: url smtp.protonmail.ch:587/127.0.0.1 where 127.0.0.1 is "used" as the client's EHLO. According to Reference [8] SMTP does not check your domain name or IP address, so it will accept almost anything for EHLO.
The code below uses cURL in a bash function to send a status email:
#!/bin/bash
# -------- function send_email ---------
send_email () {
MESSAGE="From: ray@franco.ms\nTo: ray@rayfranco.com\nSubject: Server Status\n\nThe Server is $1 - $(date)\n"
curl --verbose \
--url smtp.protonmail.ch:587/127.0.0.1 \
--ssl-reqd \
--user 'ray@franco.ms:My_Redacted_SMTP_Password' \
--mail-from ray@franco.ms \
--mail-rcpt ray@rayfranco.com \
--upload-file <(echo -e "$MESSAGE")
}
# ---------- main script --------
send_email UP
Note the last line of the send_email function is:
--upload-file <(echo -e $MESSAGE)
which is "Process Substitution", <(...) - not redirection and command line substitution.
The verbose output is:
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0* Host smtp.protonmail.ch:587 was resolved.
* IPv6: (none)
* IPv4: 176.119.200.135, 185.70.42.135, 185.205.70.135
* Trying 176.119.200.135:587...
* Connected to smtp.protonmail.ch (176.119.200.135) port 587
0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0< 220 mailsubzur1002.protonmail.ch ESMTP Postfix
> EHLO 127.0.0.1
< 250-mailsubzur1002.protonmail.ch
< 250-PIPELINING
< 250-SIZE 36480000
< 250-STARTTLS
< 250-ENHANCEDSTATUSCODES
< 250-8BITMIME
< 250 CHUNKING
> STARTTLS
< 220 2.0.0 Ready to start TLS
} [5 bytes data]
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
} [1555 bytes data]
* CAfile: /etc/ssl/certs/ca-certificates.crt
* CApath: /etc/ssl/certs
0 0 0 0 0 0 0 0 --:--:-- 0:00:01 --:--:-- 0{ [5 bytes data]
* TLSv1.3 (IN), TLS handshake, Server hello (2):
{ [122 bytes data]
* TLSv1.3 (IN), TLS change cipher, Change cipher spec (1):
{ [1 bytes data]
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
{ [6 bytes data]
* TLSv1.3 (IN), TLS handshake, Certificate (11):
{ [4405 bytes data]
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
{ [520 bytes data]
* TLSv1.3 (IN), TLS handshake, Finished (20):
{ [52 bytes data]
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
} [1 bytes data]
* TLSv1.3 (OUT), TLS handshake, Finished (20):
} [52 bytes data]
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384 / x25519 / RSASSA-PSS
* Server certificate:
* subject: CN=protonmail.com
* start date: Sep 7 13:14:28 2026 GMT
* expire date: Dec 6 13:14:27 2026 GMT
* subjectAltName: host "smtp.protonmail.ch" matched cert's "*.protonmail.ch"
* issuer: C=US; O=Let's Encrypt; CN=YR1
* SSL certificate verify ok.
* Certificate level 0: Public key type RSA (4096/152 Bits/secBits), signed using sha256WithRSAEncryption
* Certificate level 1: Public key type RSA (2048/112 Bits/secBits), signed using sha256WithRSAEncryption
* Certificate level 2: Public key type RSA (4096/152 Bits/secBits), signed using sha256WithRSAEncryption
* Certificate level 3: Public key type RSA (4096/152 Bits/secBits), signed using sha256WithRSAEncryption
* Connected to smtp.protonmail.ch (176.119.200.135) port 587
} [5 bytes data]
> EHLO 127.0.0.1
{ [5 bytes data]
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
{ [233 bytes data]
< 250-mailsubzur1002.protonmail.ch
< 250-PIPELINING
< 250-SIZE 36480000
< 250-AUTH PLAIN LOGIN
< 250-ENHANCEDSTATUSCODES
< 250-8BITMIME
< 250 CHUNKING
} [5 bytes data]
> AUTH PLAIN
{ [5 bytes data]
< 334
} [5 bytes data]
> AHJheUBmcmFuY28ubXMAWE00MlhGUjZHMlFDNDlRTA==
0 0 0 0 0 0 0 0 --:--:-- 0:00:02 --:--:-- 0{ [5 bytes data]
< 235 2.7.0 Authentication successful
} [5 bytes data]
> MAIL FROM:
{ [5 bytes data]
< 250 2.1.0 Ok
} [5 bytes data]
> RCPT TO:
{ [5 bytes data]
< 250 2.1.5 Ok
} [5 bytes data]
> DATA
0 0 0 0 0 0 0 0 --:--:-- 0:00:03 --:--:-- 0{ [5 bytes data]
< 354 End data with .
} [5 bytes data]
* upload completely sent off: 123 bytes
100 123 0 0 0 123 0 26 --:--:-- 0:00:04 --:--:-- 26{ [5 bytes data]
< 250 2.0.0 Ok: queued as 4hrQTq4HDGz1DF4m
100 123 0 0 0 123 0 24 --:--:-- 0:00:05 --:--:-- 25
* Connection #0 to host smtp.protonmail.ch left intact
curl SMTP has a catch 22. You have to use the --upload-file option, which means you have to write and read to disk, or you have to use "Process Substitution". Unfortunately, "Process Substitution" is not defined for Portable Operating System Interface (POSIX) shells (e.g. -dash). Also, POSIX shells do not have the -e option for the echo command. You might be able to get around this by storing the message in a temporary ram disk file such as "/dev/shm" (shared memory).
Posix Shells do not have: