nixpulvis

"Visual" Voicemail from Ruby 910 words published on April 27, 2021.

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

I have a PinePhone which I’ve recently been trying out running postmarketOS 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 library to fetch the voicemail(s).

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:

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.

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. 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.