It's a bit more complicated than that, since you've got to keep track of the volume that the message is on, too. This is a Q&D that I put together for 3.1.x, but it'll probably work with 4.x, too. Run it as the zimbra user.
Code:
#!/usr/bin/perl
# OK, there's 2 MAILBOX_*_BITS values in the VOLUME table.
# Take the mailbox ID, right-shift it by MAILBOX_BITS, and take the lowest MAILBOX_GROUP_BITS of the result.
# That's your mailbox hash.
# Take the message ID, right-shift it by FILE_BITS, and take the lowest FILE_GROUP_BITS of the result. That's your msgid hash.
# I think.
# <mbx-hash>/<mbx-id>/msg/<msgid-hash>/<msgid>-<mod_content>.msg
my ($fbits, $fgbits, $mbits, $mgbits, $basepath) = split (' ',`echo "select file_bits, file_group_bits, mailbox_bits, mailbox_group_bits, path from volume where type='1'" | mysql -N zimbra`);
chomp $basepath;
my $mbmask = sprintf "1" x $mgbits;
my $fmask = sprintf "1" x $fgbits;
foreach (`echo "select id, account_id, comment from mailbox" | mysql -N zimbra`) {
chomp;
my $path = "$basepath/";
my ($id, $aid, $nm) = (split);
my $mbhash = $id >> $mbits;
$mbhash &= $mbmask;
$path .= $mbhash."/".$id."/msg/";
foreach my $msgstuff (`echo "select id,mod_content from mail_item where blob_digest is not null;" | mysql -N mailbox${id}`) {
chomp $msgstuff;
if ($msgstuff eq "") {next;}
my ($msgid, $modContent) = split (' ',$msgstuff);
my $msghash = $msgid >> $fbits;
$msghash &= $fmask;
my $nm = $msgid;
if ($modContent) {$nm .= "-$modContent";}
my $npath = $path.$msghash."/".$nm.".msg";
print $npath."\n";
}
}