---
title: '"Visual" Voicemail from Ruby'
date: '2021-04-27'
words: 638
collection: ramblings
---

_Note: This post is specific to AT&T, but may be similar across other providers._

I have a [PinePhone][pine-phone] which I've recently been trying out running
[postmarketOS][postmarket-os] on. It's got a number of problems, but without
getting caught up listing them all, I'll talk briefly about "visual" voicemail
(VVM).

Yesterday I received the following SMS text message (redacted for privacy):

```
MBOXUPDATE?-m=1;server=vvm.mobile.att.net;port=143;-name=4:XXXXXXXXXX:A:XXXXX:ms-01:NAT:XXXXX
```

Neat! But what is behind `vvm.mobile.att.net`? After a quick search online and
a few threads listing some kind of status message, I decided to text back the
string `STATUS`, and sure enough it responded with the following
(formatted/elided for clarity):

```
//-
VVM:STATUS:
  ...
  srv=2:vvm.mobile.att.net;
  ...
  smtp_u=4:XXXXXXXXXX:A:XXXXX:ms01:NAT:XXXXX;
  smpt_pw=XXXXXXX;
  ...
```

So now we know this thing is using SMTP to send messages, that it uses the
phone number as the username, and that the password is the PIN I setup a while
ago.  This should make it relatively easy to download the voicemail with IMAP.
I have no idea how to reset this PIN from Ruby at the moment however.

At this point, you could probably plug in the username and password to your
favorite email client, but I want to write a small script to retrieve these for
me as audio files directly. So, it's off to Ruby's STD [`net/imap`][net/imap]
library to fetch the voicemail(s).

```ruby
require 'net/imap'

# Authenticate with the remote IMAP server
#
# Fetches must be done within a relatively short time window
# after this request completes.
def imap_login(phone_number, pin, host: 'vvm.mobile.att.net', port: 143)
  imap = Net::IMAP.new(host, port: port)
  imap.login(phone_number, pin)
  imap
end

# Print all the mailboxes in the authenticated IMAP server
def fetch_mailboxes(imap)
  imap.list("", "*").each do |mailbox|
    puts mailbox.name
  end
end

# Print the messages in an authenticated IMAP server's mailbox
def fetch_mailbox(imap, mailbox)
  imap.examine(mailbox)
  imap.fetch(1..-1, ["ENVELOPE"]).each do |message|
    envelope = message.attr["ENVELOPE"]
    puts "[#{message.seqno}] #{envelope.date}\t#{envelope.from[0].name}"
  end
end
```

Create an `imap` variable for interacting with the server:

```ruby
imap = imap_login('XXXXXXXXXX', 'XXXXXXX')
```

By calling `fetch_mailboxes(imap)` I can tell there's a mailbox conveniently
named `INBOX`, along with `Trash`, and `Greetings`, something I can only assume
holds the voicemail message I recorded a while ago.

So, sure enough calling `fetch_mailbox(imap, "INBOX")` lists all five
voicemails in my inbox, along with the `seqno`, date and sender's phone number.

```
[1] Sun, 4 Apr 2021 18:12:07 -0400    XXXXXXXXXX 
[2] Fri, 9 Apr 2021 11:19:48 -0400    XXXXXXXXXX 
[3] Sat, 10 Apr 2021 13:16:16 -0400   XXXXXXXXXX 
[4] Fri, 16 Apr 2021 19:57:01 -0400   XXXXXXXXXX 
[5] Tue, 27 Apr 2021 13:21:39 -0400   XXXXXXXXXX 
```

Great, now all I need to do is decode the body of the messages and play it from
my speakers.

To do this, first we fetch the `BODY` section, which contains the actual
voicemail audio data. I'm assuming that there will always be a single part to
this structure, so we also directly request the first part of the body with
`BODY[1]`. By looking at the part's `encoding` we know that this message is
base 64 encoded, so after a quick `decode64` call we can write the file to
disk.

```ruby
require 'base64'

# Fetch and decode a single voicemail saving it as `voice.amr`
def fetch_mailbox_message(imap, mailbox, seqno)
  imap.examine(mailbox)
  imap.fetch(seqno, ["BODY", "BODY[1]"]).map do |message|
    part_body = Base64.decode64(message.attr["BODY[1]"])
    File.open('voice.amr', "w") do |file|
      file.write(part_body)
    end
  end
end
```

Finally, just call `fetch_mailbox_message(imap, "INBOX", seqno)` and we can
play the message with `vlc voice.amr` (or whatever else you have that knows
what an AMR file is, I sure don't).

Now I'll be honest about my complete ignorance of the Open Mobile Terminal
Platform (OMTP) VVM [specification][spec]. Partially because it was faster to
just work backwards, and partially because I'm aware that some providers do not
follow the spec. It stands to reason however, if you run into problems with
this post, that the spec may guide you towards a more complete solution.

Hopefully this helps someone else who's stuck trying to listen to their
voicemails.


[pine-phone]: https://www.pine64.org/pinephone
[postmarket-os]: https://postmarketos.org
[spec]: http://www.omtp.org/OMTP_VVM_Specification_v1_3_Final.pdf
[net/imap]: https://ruby-doc.org/stdlib-2.5.1/libdoc/net/imap/rdoc/Net/IMAP.html
