Mail: SMTP & IMAP

RunHacks SMTP and IMAP mail access

Your RunHacks mailbox, in your own mail client or your own script. Create an API key, use it as the password, send and read over TLS.

What this is

RunHacks runs a mail server on mail.runhacks.sh. It speaks real SMTP for sending and real IMAP for reading. Point Thunderbird, mutt, or your own script at it and it behaves the way a mail server behaves, because it is one. It serves the same mailbox the Mail app shows you inside RunHacks OS: challenge characters send you mail there, and mail you send from a client reaches them exactly as if you had used the Mail app.

It is a closed system

RunHacks mail is sealed off from the public mail network in both directions. There is no MX record for runhacks.sh, so nothing on the internet can route mail here, and the server has no outbound relay, so nothing you send can leave. Mail from your Gmail will never arrive in your RunHacks inbox, and you cannot mail your Gmail from your RunHacks mailbox.

Every address on both ends of a message has to exist inside RunHacks: your own mailbox, another player's, or a character at a challenge domain that has been opened up to your account. Anything else is refused outright at RCPT TO rather than accepted and quietly dropped, so you always find out immediately. See Limits for the exact refusal.

This is deliberate. A closed loop means you can be as aggressive as you like with the mail protocol, spoofing headers, forging senders, pretexting and phishing a character, without any of it escaping to reach a real person.

Your username is the part before @runhacks.sh, the same name you use for SFTP. Your password is a RunHacks API key. There is no separate mail password to forget.

Ports

Four ports, one server behind all of them:

Host   mail.runhacks.sh

SMTPS  465    implicit TLS      send      recommended
SMTP   2525   plaintext         send
IMAPS  993    implicit TLS      read      recommended
IMAP   1143   plaintext         read

Use 465 and 993. The plaintext ports are not a fallback for when TLS is inconvenient, they are there so you can watch the protocol go by with nc and read what a mail server actually says. Anything you do on them, including your API key, crosses the network in the clear.

The TLS ports are implicit TLS: the connection is encrypted from the first byte. There is no STARTTLS. If your client offers "STARTTLS" and "SSL/TLS" as options, pick SSL/TLS. A client set to STARTTLS on port 465 will hang, and a client set to STARTTLS on 2525 will either fail or quietly send your credentials unencrypted.

Get an API key

Open RunHacks OS, press alt+k for the Keys app, press n, and name the key after the thing that will use it. The name is what you will use later to work out which key to revoke.

The secret appears once, on the screen right after you create it. Nothing on RunHacks can show it to you again, because we do not store it in a readable form. Copy it then, or create another key later.

One key per client, for the same reason as one SSH key per device: a key that exists in exactly one place can be revoked without breaking anything else you own.

Your API key is a password

RunHacks will never ask you for an API key, and neither will any legitimate service. Not support, not a password reset, not a "verification" step, not a challenge character who sounds like they have authority. If anything asks you for one, that is the attack, and noticing it is the skill.

A key looks like rh_ followed by a long random string. Anything holding one can read your mail and send mail as you. Keep it out of source control, out of screenshots, and out of the challenge sites you are testing. If one leaks, revoke it in the Keys app with x and create a replacement.

Verify the server

The TLS ports present a certificate for mail.runhacks.sh issued by Let's Encrypt. Your client checks it for you, which is most of the value of using them. Do not click through a certificate warning to reach a mail server you are about to hand a credential to.

Look at what the server is presenting before you trust it:

openssl s_client -connect mail.runhacks.sh:465 -servername mail.runhacks.sh

The chain should end at ISRG Root X1 and the subject should be mail.runhacks.sh. Certificates rotate, so the fingerprint is not a fixed value you can memorize the way the SSH host key is. Trust the chain, not a fingerprint you wrote down last month.

Configure a mail client

Thunderbird, Account Settings, Add Mail Account, then Configure Manually:

Incoming   IMAP   mail.runhacks.sh   993   SSL/TLS   Normal password
Outgoing   SMTP   mail.runhacks.sh   465   SSL/TLS   Normal password
Username   your-username        (the name only, not the full address)
Password   rh_your_api_key

Apple Mail, Add Account, Other Mail Account. Enter you@runhacks.sh and your API key as the password, then when it asks, choose IMAP and fill in the same hosts and ports as above. Apple Mail will try to guess the settings first and will guess wrong, so expect to correct it.

mutt, in ~/.muttrc:

set imap_user = "your-username"
set imap_pass = "rh_your_api_key"
set folder = "imaps://mail.runhacks.sh:993/"
set spoolfile = "+INBOX"
set record = "+Sent"
set trash = "+Trash"
set postponed = "+Archive"
set smtp_url = "smtps://your-username@mail.runhacks.sh:465/"
set smtp_pass = "rh_your_api_key"
set ssl_force_tls = yes

Whatever the client, the two settings people get wrong are the same two: the username is the name only and not the full address, and the security setting is SSL/TLS and not STARTTLS.

Send from code

Python, with SMTP_SSL for implicit TLS:

import smtplib
from email.mime.text import MIMEText

msg = MIMEText("Your message body")
msg["Subject"] = "Subject line"
msg["From"] = "you@runhacks.sh"
msg["To"] = "recipient@runhacks.sh"

with smtplib.SMTP_SSL("mail.runhacks.sh", 465) as server:
    server.login("your-username", "rh_your_api_key")
    server.send_message(msg)

Node, with nodemailer. Note secure: true, which is what selects implicit TLS:

const nodemailer = require("nodemailer");

const transporter = nodemailer.createTransport({
  host: "mail.runhacks.sh",
  port: 465,
  secure: true,
  auth: { user: "your-username", pass: "rh_your_api_key" },
});

await transporter.sendMail({
  from: "you@runhacks.sh",
  to: "recipient@runhacks.sh",
  subject: "Subject line",
  text: "Your message body",
});

swaks, for one-off probing from a terminal:

swaks --to recipient@runhacks.sh \
      --from you@runhacks.sh \
      --server mail.runhacks.sh:465 --tlsc \
      --auth LOGIN --auth-user your-username --auth-password rh_your_api_key \
      --header "Subject: Test" --body "Your message"

Go, with tls.Dial, since smtp.SendMail assumes STARTTLS:

conn, err := tls.Dial("tcp", "mail.runhacks.sh:465", nil)
c, err := smtp.NewClient(conn, "mail.runhacks.sh")
c.Auth(smtp.PlainAuth("", "your-username", "rh_your_api_key", "mail.runhacks.sh"))

Read from code

import imaplib

imap = imaplib.IMAP4_SSL("mail.runhacks.sh", 993)
imap.login("your-username", "rh_your_api_key")

imap.select("INBOX")
status, messages = imap.search(None, "ALL")
for num in messages[0].split():
    status, data = imap.fetch(num, "(RFC822)")
    print(data[0][1].decode())

imap.logout()

There is no IDLE, so a client that wants to know about new mail has to poll. Poll on a sane interval. A tight loop against the mailbox is the kind of automated hammering the Rules of Engagement ask you not to do.

What the server supports

Ask it yourself, which is a better habit than trusting this page:

printf 'a1 CAPABILITY\r\na2 LOGOUT\r\n' | openssl s_client -quiet \
    -connect mail.runhacks.sh:993 -servername mail.runhacks.sh

It answers IMAP4rev1 AUTH=PLAIN AUTH=LOGIN SASL-IR LITERAL+. Four mailboxes exist and no others can be created: INBOX, Sent, Trash, and Archive. Flags that persist are \Seen, \Flagged, and \Deleted. Deleting moves a message to Trash, and EXPUNGE on Trash is what actually removes it.

On the SMTP side, EHLO reports AUTH LOGIN PLAIN and advertises a SIZE of 32 MiB. That number comes from the SMTP library's default and is not the limit that applies to you. The real limit is below.

Limits

Message size          5 KiB   subject plus body, per message
Recipients            20      to, cc and bcc combined
SMTP connections      4 per minute
Recipient domains     runhacks.sh, plus any a challenge opens up

The size limit is small on purpose. This is a mailbox for talking to challenge characters, not for moving files. Over it, the server answers 554 5.0.0 Failed to deliver: Message too large with the byte count it measured.

Mail to an address outside the allowed domains is refused at RCPT TO with 550 5.1.1 Recipient domain not allowed, and the refusal lists the domains that are currently allowed. That list is the authoritative answer, not this page: challenges add their own fiction domains to it as you unlock them.

Mail to another player at @runhacks.sh lands in their inbox. Mail to a character at an allowed challenge domain is delivered into the fiction, with no copy in your Sent folder's counterpart on the other side. Every other address, including your own Gmail, a colleague, or a real company, is refused, because there is nowhere outside RunHacks for it to go.

Reading the refusals

The server tries to tell you what went wrong. The codes worth recognizing:

235   authentication accepted
250   message accepted
421   rate limited, try again later
530   you have not authenticated yet
535   authentication failed
550   recipient domain not allowed, or no such user
554   accepted the envelope, refused the message

A 535 almost always means one of three things: the key is revoked, you pasted the key with a trailing newline, or you used your full address as the username instead of the name part. Check the Keys app for the first, and your client config for the other two.

Revoking a key

Open RunHacks OS, press alt+k for the Keys app, select the key by the name you gave it, and press x. Anything still using it stops working immediately: SMTP, IMAP, and API calls all authenticate with the same key.

What we store

Per key: the name you gave it, the visible prefix, when you created it, when it was last used, and a hash of the secret. We do not have the secret itself and cannot recover it for you.

Your mail is stored the way mail is stored: we can see it, and so can the challenge engine that scores you. Treat the mailbox as a game surface rather than a private channel, and do not put anything in it you would not want us to have.

In scope

mail.runhacks.sh is in scope for your own account, on the same terms as the file server. Inspect the protocol, see what the server refuses and how it refuses it, try to make it accept something it should not, and try to make it hand you something that is not yours.

Other players' mailboxes are not in scope. Player mailboxes are real mailboxes. If you find a way to read one, or to send mail as somebody else, that is a real vulnerability rather than a flag. Stop and mail security@runhacks.sh.

RunHacks OS v1.0 · kernel rh-tui 0.9.2 · build 2026.08.25  ·  about RunHacks