- Go 98.9%
- Shell 0.6%
- Dockerfile 0.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
The contact list only includes names. Phone numbers are on the encrypted card, and a failed signature was discarding that card. Co-authored-by: Cursor <cursoragent@cursor.com> |
||
| .forgejo/workflows | ||
| cmd/mautrix-aasms | ||
| pkg | ||
| .gitignore | ||
| build.sh | ||
| Dockerfile | ||
| example-config.yaml | ||
| go.mod | ||
| go.sum | ||
| README.md | ||
mautrix-aasms
A Matrix / Beeper puppeting bridge for AAISP SMS, built on mautrix-go bridgev2.
Send and receive plain-text SMS through one or more AA VoIP numbers. Each number is a separate bridge login; replies always go out from the number that owns that chat.
Features (v1)
- Inbound SMS via a shared HTTPS webhook (AAISP HTTP POST)
- Outbound SMS via
https://sms.aa.net.uk/sms.cgi - Multiple AA numbers for one Matrix/Beeper user
- Bot login with number + outgoing SMS password
- Start new chats with
start-chat/ Beeper provisioning (ResolveIdentifier) - SQLite persistence (bridgev2 default)
- End-to-bridge encryption (Megolm) so a Beeper homeserver stores ciphertext
Not in v1: delivery reports, MMS/media, SMS groups, alpha sender IDs, history backfill.
Requirements
- The published container image, or Go 1.27.3+ to build from source
- A Beeper account (or any Matrix homeserver that can run an appservice)
- One or more AAISP numbers that can send and receive SMS (07 recommended for inbound)
- A public HTTPS URL reaching the bridge (reverse proxy, Cloudflare Tunnel, etc.)
Run
Published image (package), linux/amd64 and linux/arm64:
docker pull git.gac.sh/george/mautrix-aasms:latest
latest tracks the default branch. Each push is also tagged with its full commit SHA.
Generate a config, then run the bridge with that directory mounted at /data (config and the SQLite database live there; the bridge listens on port 29323):
mkdir -p data
docker run --rm -v "$PWD/data:/data" \
git.gac.sh/george/mautrix-aasms:latest -e -c /data/config.yaml
docker run --rm -v "$PWD/data:/data" -p 29323:29323 \
git.gac.sh/george/mautrix-aasms:latest
Build
From source (pure-Go olm via -tags goolm; SQLite still needs cgo):
./build.sh
# produces ./mautrix-aasms
./mautrix-aasms -e -c config.yaml
Or build the image locally:
docker build -t mautrix-aasms .
Beeper setup (bbctl)
-
Install and log in with bbctl.
-
Generate a bridgev2 config:
bbctl config --type bridgev2 sh-aasms -
Merge / set at least:
appservice.public_address— public HTTPS base URL of this bridgenetwork.webhook_secret— random secret of at least 32 characters (openssl rand -hex 32)- homeserver / appservice credentials from bbctl
-
Run the bridge with that config (binary or container). Keep it running (
tmux/systemd/Docker). -
In Beeper, DM the bridge bot (localpart is typically
aasmsbot/@sh-aasmsbot:beeper.localdepending on your bbctl name) and run login. -
Enter the AA number and the outgoing SMS password from the AA control pages. Repeat login for each number you want to use.
Starting chats
- Bot:
start-chat <phone>(optionally prefix with a login ID if you have several) - Or use Beeper’s start-DM / phone lookup UI if it calls the provisioning API — this bridge implements
ResolveIdentifier.
Chats are scoped to the current login. A DM started under +447…A always sends as A; the same person under number B is a separate room.
Contact names
Chats are titled with the phone number until Proton Contacts is connected. In the management room with the bridge bot:
contacts-loginasks for the Proton username, then the password, then an authenticator code if the account has one. A separate mailbox password is asked for only when the account uses one. The bridge tries to remove password and code messages from the room. If Proton asks for human verification, the bot sends a verify.proton.me link. Complete that page in a browser, then senddone.contacts-syncdownloads the address book again and renames chats.contacts-statusshows whether a Proton session is connected.contacts-logoutdrops the session. Chats go back to phone numbers.
The bridge stores the Proton session in its database, restores it on startup, and refreshes contacts hourly. Each phone number on a contact is matched to that contact's name. Proton shows the session as an unknown application. A security key with no authenticator code cannot be completed in the bot.
The stored session can decrypt Proton mail as well as contacts. It is kept in the same database as the SMS passwords.
AAISP control pages
For each logged-in number:
-
Open the number → Incoming → SMS inbound target.
-
Set a single HTTPS URL (POST — do not end the URL with
?or&):https://bridge.example.com/_aasms/sms?token=YOUR_SECRETUse the same
tokenasnetwork.webhook_secret. All numbers can share this URL; the bridge routes by theda(destination) field. -
Ensure the outgoing SMS password matches what you entered at login.
API reference: SMS API.
End-to-bridge encryption
Portal rooms and the bridge management room use Matrix Megolm (m.megolm.v1.aes-sha2). The bridge encrypts and decrypts on the machine where it runs. Hungryserv stores ciphertext for those events.
bbctl config --type bridgev2 already sets encryption.allow, default, require, and appservice mode. A config generated with -e leaves encryption.allow false. On startup, if homeserver.software is hungry and encryption.allow is false, the bridge applies the same settings official self-hosted bridges use (appservice Megolm, cross-signed device trust, and key deletion after use). An explicit encryption.allow: true is kept as written.
Rooms created before encryption was on are switched to encrypted at the next start. Events already stored in plaintext on the homeserver stay plaintext.
SMS between this bridge and AAISP is still the carrier's plain text. The bridge reads the message in order to send it.
On a normal Synapse, set encryption.allow and encryption.default to true and leave encryption.appservice false. See the mautrix end-to-bridge encryption docs. The binary is built with pure-Go olm (-tags goolm).
Message model
| Direction | Behaviour |
|---|---|
| Inbound | Webhook → normalize oa/da → find login for da → DM portal for oa |
| Outbound | Matrix text in that DM → sms.cgi as the login’s number |
| Dedupe | (oa, da, ud, scts) ignored for ~10 minutes on retries |
| Empty body | Logged and dropped (HTTP 200 so AAISP does not retry forever) |
Unknown da |
HTTP 200 + log (no Matrix event) |
| Send failure | ERR:… from AAISP surfaced as a Matrix send error |
Numbers are stored as E.164 (+447…). UK national (07…) and 447… are accepted on input.
Config notes
Network section (also embedded via -e):
network:
webhook_secret: change-me
Inbound SMS is rejected while webhook_secret is shorter than 32 characters, which includes the change-me default. Generate one with openssl rand -hex 32.
appservice.public_address must be set or the bridge will refuse to start (webhook registration depends on it).
License
See repository metadata on git.sr.ht/~gc/mautrix-aasms.