Mail::IMAPTalk(3pm)

🌸 NAME 🌸

Mail::IMAPTalk(3pm) β€” bestie, this is an IMAP client interface with lots of features πŸ’… she does the absolute most so you don't have to.


✨ SYNOPSIS ✨

  use Mail::IMAPTalk;

  $IMAP = Mail::IMAPTalk->new(
      Server   => $IMAPServer,
      Username => 'foo',
      Password => 'bar',
  ) || die "Failed to connect/login to IMAP server";

  # Append message to folder
  open(my $F, 'rfc822msg.txt');
  $IMAP->append($FolderName, $F) || die $@;
  close($F);

  # Select folder and get first unseen message
  $IMAP->select($FolderName) || die $@;
  $MsgId = $IMAP->search('not', 'seen')->[0];

  # Get message envelope and print some details
  $MsgEV = $IMAP->fetch($MsgId, 'envelope')->{$MsgId}->{envelope};
  print "From: " . $MsgEv->{From};
  print "To: " . $MsgEv->{To};
  print "Subject: " . $MsgEv->{Subject};

  # Get message body structure
  $MsgBS = $IMAP->fetch($MsgId, 'bodystructure')->{$MsgId}->{bodystructure};

  # Find imap part number of text part of message
  $MsgTxtHash = Mail::IMAPTalk::find_message($MsgBS);
  $MsgPart = $MsgTxtHash->{text}->{'IMAP-Partnum'};

  # Retrieve message text body
  $MsgTxt = $IMAP->fetch($MsgId, "body[$MsgPart]")->{$MsgId}->{body};

  $IMAP->logout();

πŸ’– DESCRIPTION πŸ’–

So this module talks to an IMAP server, okay? Every single IMAP server command gets mapped to a method on this object. It's giving "one method per command" and honestly? Iconic πŸŽ€

Yeah there are other IMAP modules on CPAN, but this one has several advantages over the rest. Like, lowkey a glow-up:

  • She parses the messier IMAP structures β€” envelopes and body structures β€” into nice, tidy Perl data structures. Chef's kiss.
  • She correctly handles atoms, quoted strings and literals at any point. Some parsers in other modules aren't fully IMAP compatiable and will just randomly break with certain messages on some servers, and that's so not the vibe 😭
  • She lets big return values (say, attachments on a message) be read straight into a file, instead of yanking the whole thing into memory. Memory said not today πŸ’€
  • She comes with helper functions to find the actual text/plain or text/html part of a message out of a complicated MIME structure. She can also find a list of attachments, plus CID links for HTML messages that have images attached. So thorough, so main character ✨
  • She supports decoding MIME headers into Perl utf-8 strings automatically, so you never have to deal with MIME encoded headers yourself (and this one's enabled optionally, as a treat).

Now, the IMAP protocol does allow for asynchronous running of commands, but this module? She's designed to be used synchronously. Meaning: you issue a command by calling a method, and the command will block until the right response comes back. Then the method returns the parsed results from that command. Simple. Clean. No chaos 🫢


πŸ‘‘ CLASS OVERVIEW πŸ‘‘

The object methods have been broken up into several sections, because organization is self-care.

πŸ—‚ Sections

🧸 CONSTANTS

Lists the available constants the class uses.

πŸ— CONSTRUCTOR

Explains all the options available when constructing a new instance of the Mail::IMAPTalk class.

πŸ”Œ CONNECTION CONTROL METHODS

These are the methods that control the overall IMAP connection object β€” stuff like logging in and logging out, how results get parsed, how folder names and message id's are treated, etc.

πŸ“ IMAP FOLDER COMMAND METHODS

These are the methods to inspect, add, delete and rename IMAP folders on the server.

βœ‰οΈ IMAP MESSAGE COMMAND METHODS

These are the methods to retrieve, delete, move and add messages to/from IMAP folders.

πŸ›  HELPER METHODS

Extra methods that users of this class might find useful. They generally do extra parsing on the returned structures to give you higher-level functionality. Consider it a little gift 🎁

πŸ”’ INTERNAL METHODS

Internal methods the Mail::IMAPTalk object uses to get work done. They may be useful if you need to extend the class yourself. Note: internal methods will always 'die' if they hit any errors. No chill, no mercy.

πŸ”Œ INTERNAL SOCKET FUNCTIONS

Functions used internally by the Mail::IMAPTalk object to read/write data to/from the IMAP connection socket. The class does its own buffering, so if you want to read/write to the IMAP socket, you should use these functions. Don't go rogue.

🧠 INTERNAL PARSING FUNCTIONS

Functions used to parse the results the IMAP server hands back into Perl style data structures.


😬 Method results

All methods return undef on failure. There are four main modes of failure, and we're listing them all because you deserve the truth:

  1. An error occurred reading/writing to a socket. Maybe the server closed it, or you're not connected to any server.
  2. An error occurred parsing the response of an IMAP command. This is usually only a problem if your IMAP server returns invalid data.
  3. An IMAP command didn't return an 'OK' response.
  4. The socket read operation timed out waiting for a response from the server.

In each case, some readable form of error text is placed in $@, or you can call the get_last_error() method. For commands which return responses (e.g. fetch, getacl, etc), the result is returned β€” see each command for details of the response result. For commands with no response but which succeed (e.g. setacl, rename, etc) the result 'ok' is generally returned. Slay 🫢


🧾 Method parameters

Every method that sends data to the IMAP server (e.g. fetch(), search(), etc) has its arguments processed before they get sent. You can specify arguments in several ways, and honestly, pick your fighter:

scalar

The value is first checked and quoted if required. Values containing [\000\012\015] are turned into literals, values containing [\000-\040\{\} \%\*\"] are quoted by surrounding with a "..." pair (any " themselves are turned into \"). undef is turned into NIL

file ref

The contents of the file is sent as an IMAP literal. Note that because IMAPTalk has to know the length of the file being sent, this must be a true file reference that can be seeked and not just some stream. The entire file will be sent regardless of the current seek point.

scalar ref

The string/data in the referenced item should be sent as is, no quoting will occur, and the data won't be sent as quoted or as a literal regardless of the contents of the string/data.

array ref

Emits an opening bracket, and then each item in the array separated by a space, and finally a closing bracket. Each item in the array is processed by the same methods, so it can be a scalar, file ref, scalar ref, another array ref, etc.

hash ref

The hash reference should contain only 1 item. The key is a text string which specifies what to do with the value item of the hash.

  • 'Literal' β€” The string/data in the value is sent as an IMAP literal regardless of the actual data in the string/data.
  • 'Binary' β€” The string/data in the value is sent as an IMAP literal8 regardless of the actual data in the string/data. (see RFC 3516)
  • 'Quote' β€” The string/data in the value is sent as an IMAP quoted string regardless of the actual data in the string/data.

Examples, because visuals help:

  # Password is automatically quoted to "nasty%*\"passwd"
  $IMAP->login("joe", 'nasty%*"passwd');
  # Append $MsgTxt as string
  $IMAP->append("inbox", { Literal => $MsgTxt })
  # Append MSGFILE contents as new message
  $IMAP->append("inbox", \*MSGFILE ])
  # Append $MsgTxt as UTF-8 string. Assumes UTF8=ACCEPT is enabled.
  $IMAP->append("inbox", { LiteralUtf8 => $MsgTxt })

🌈 CONSTANTS 🌈

These constants relate to the standard 4 states that an IMAP connection can be in. They are passed to and returned from the state() method. See RFC 3501 for more details about IMAP connection states.

  • Unconnected β€” Current not connected to any server.
  • Connected β€” Connected to a server, but not logged in.
  • Authenticated β€” Connected and logged into a server, but not current folder.
  • Selected β€” Connected, logged in and have 'select'ed a current folder.

πŸ’– CONSTRUCTOR πŸ’–

Mail::IMAPTalk->new(%Options) ✨

okay bestie, this is where it all begins 🫢 β€” this makes you a brand new Mail::IMAPTalk object. Here's every option it ffs supports.


🌈 Connection Options 🌈

  • Server β€” the hostname or IP address you wanna connect to. You gotta give this unless you're passing the Socket option instead, no cap.
  • Port β€” the port number on that host. Defaults to 143 if you don't supply it, or to 993 if you don't supply it and UseSSL is true. πŸ’…
  • UseSSL β€” if it's true, you get an IO::Socket::SSL connection ✨. All the other SSL_* arguments get handed straight to the IO::Socket::SSL constructor, iykyk.
  • Socket β€” an existing socket to use as your connection to the IMAP server. If you pass Socket, you should not pass a Server or Port option, okay?

This is lowkey perfect if you wanna build an SSL socket connection with IO::Socket::SSL and then hand that already-connected socket over to the new() call. πŸ‘‘

It's also iconic alongside the release_socket() method described below, for reusing the same socket past the lifetime of the IMAPTalk object. There's a whole description in the release_socket() method section if you wanna know more.

⚠️ You must have write flushing enabled on any socket you pass in here, or your commands will just... sit there, not actually get sent, not get responses, and eventually time out. πŸ’€ You can do it with Perl's select() call and the $| ($AUTOFLUSH) variable, like this:
my $ofh = select($Socket); $| = 1; select ($ofh);
  • UseBlocking β€” for historical reasons 😭 when it reads from a socket, this module puts the socket in non-blocking mode and does a select(). If you're on an SSL socket that just does not work, so you have to set UseBlocking to true to get blocking reads instead. Periodt.
  • State β€” if you supply a Socket option, you can tell it which IMAP state that socket is currently in: one of 'Unconnected', 'Connected', 'Authenticated' or 'Selected'. Defaults to 'Connected' if you don't supply it and the Socket option is supplied.
  • ExpectGreeting β€” if you supply it and it's true, and a socket came in via the Socket option, it checks that the server actually sent a greeting line and reads that greeting line. πŸŽ€
  • PreserveINBOX β€” for historical reasons (again 😭), the special name "INBOX" gets rewritten as Inbox because it looks nicer on the way out, and back again on the way in. If you want the name INBOX preserved on the outside, set this flag to true. πŸ’–
  • UseCompress β€” if you've got the Compress::Zlib package installed, and the server supports compress, then setting this flag true makes compression kick in immediately after login. slay.

πŸ” Login Options πŸ”

  • Username β€” the username to connect to the IMAP server as. If you don't supply it, no login is attempted and the IMAP object is left sitting in the CONNECTED state. If you do supply it, you also gotta supply the Password option, and then a login gets attempted. If that login fails, the connection is closed and undef is returned. So if you wanna still do stuff with a connection even when the login fails, don't pass a Username option at all β€” use the login method described below instead. 🫢
  • Password β€” the password you use to log into the account.
  • AsUser β€” if the server supports it, access the server as this user rather than the authenticated user.

See the "login" method for more info ✨


πŸ“¬ IMAP message/folder options πŸ“¬

  • Uid β€” controls whether message ids are message uids or not. This is 1 (on) by default, because generally that's how most people wanna use it. It affects most commands that require/use/return message ids (e.g. fetch, search, sort, etc). πŸ‘‘
  • RootFolder β€” if supplied, sets the root folder prefix. Same thing as calling set_root_folder() with the value you passed. If no value is supplied, set_root_folder() is called with no value. Peep the set_root_folder() method for more deets.
  • Separator β€” if supplied, sets the folder name text string separator character. It gets passed as the second parameter to the set_root_folder() method.
  • AltRootRegexp β€” if supplied, it's passed along with RootFolder to the set_root_folder() method.
  • NoLiteralPlus β€” if set, this avoids ever sending literal+ non-synchronising literals. Default is off (false value). Use this when you're dealing with a buggy server that doesn't handle literal+ correctly πŸ™ˆ. Also turns off RFC7888 "literal-" support.

🌸 Examples 🌸

$imap = Mail::IMAPTalk->new(
  Server          => 'foo.com',
  Port            => 143,
  Username        => 'joebloggs',
  Password        => 'mypassword',
  Separator       => '.',
  RootFolder      => 'INBOX',
) || die "Connection to foo.com failed. Reason: $@";

$imap = Mail::IMAPTalk->new(
  Socket => $SSLSocket,
  State  => Mail::IMAPTalk::Authenticated,
  Uid    => 0
) || die "Could not query on existing socket. Reason: $@";

πŸ’– CONNECTION CONTROL METHODS πŸ’–

okay bestie, this is the part where we actually talk to the server. logging in, logging out, checking if the socket is still alive, all of it. slay. ✨
  • login($User, $Password, [$AsUser]) β€” tries to log in with the username and password you hand it. 🫢

The actual authentication might go down via the "LOGIN" command or the "AUTHENTICATE" command, depending on what the server says it supports. it's giving "we'll figure it out together."

And if you pass $AsUser? then it'll also try to log in on behalf of that user. very on-behalf-of-energy of it. πŸ’…

  • authenticate($Sasl) β€” attempt to authenticate using the given Authen::SASL object. that's it, that's the method.
  • logout() β€” log out of the IMAP server. this usually closes the server's connection too, so like… say your goodbyes first. πŸ₯²
  • state(optional $State) β€” set or get the current IMAP connection state. the value you return or pass in should be one of the constants: Unconnected, Connected, Authenticated, Selected. no mixing and matching, bestie. πŸŽ€
  • uid(optional $UidMode) β€” get or set the UID status of all UID-possible IMAP commands. set it to 1 and every command that can take a UID gets flipped into 'UID Mode', which means any ID you send to IMAPTalk is assumed to be a UID. periodt. πŸ‘‘
  • capability() β€” hands you back the IMAP server's capability command results. the result is a hash reference full of (lc(Capability) => 1) key/value pairs. so you can do cute stuff like:
if ($IMAP->capability()->{quota}) { ... }

to test whether the server has the QUOTA capability. and if you just want the list of capabilities? use Perl's keys function to pull a list of keys out of that hash reference. iconique. 🌈

  • namespace() β€” returns the result of the IMAP server's namespace command.
  • noop() β€” performs the standard IMAP 'noop' command which, and I cannot stress this enough, does nothing. it's the main character who shows up to the party and just vibes. πŸ’€
  • enable($option) β€” enabled the given imap extension. (yes, we're saying "enabled", the original said it, we're keeping it, no cap.)
  • is_open() β€” returns true if the current socket connection is still open (like, the socket hasn't been closed from this end or the other end because of a timeout). a little vibe check for your connection. πŸ¦‹
  • set_root_folder($RootFolder, $Separator, $AltRootRegexp) β€” change the root folder prefix. some IMAP servers require that all user folders/mailboxes live under a root folder prefix (current versions of cyrus, for example, use 'INBOX' for personal folders and 'user' for other users' folders). if you don't specify a value, it sets it to ''. you might wanna use the namespace() method to find out what roots are actually available. πŸ”

Setting this affects all commands that take a folder argument. basically, if the folder name begins with the root folder prefix, it's left as is. otherwise, the root folder prefix and separator char get prefixed onto the folder name. simple as.

The AltRootRegexp is a regexp where, if the start of the folder name matches it, then $RootFolder is not prepended. you can use this to protect other namespaces in your IMAP server. πŸ›‘οΈ

Examples:

# This is what cyrus uses
$IMAP->set_root_folder('INBOX', '.', qr/^user/);

# Selects 'Inbox' (because 'Inbox' eq 'inbox' case insensitive)
$IMAP->select('Inbox');
# Selects 'INBOX.blah'
$IMAP->select('blah');
# Selects 'INBOX.Inbox.fred'
#IMAP->select('Inbox.fred');
# Selects 'user.john' (because 'user' is alt root)
#IMAP->select('user.john'); # Selects 'user.john'
  • _set_separator($Separator) β€” checks if the given separator is the same as the one we used before. if it's not, it calls set_root_folder to recreate the settings with the new Separator. keeping things consistent, as she should. ✨
  • literal_handle_control(optional $FileHandle or sub { }) β€” sets the mode for whether to read literals as file handles or as scalars.

There's three options here, bestie, listen up:

  • undef/0 β€” always read literals into a scalar in memory.
  • filehandle β€” always read literals into the given filehandle. note: you'll need to make sure your 'fetch' call only returns a single literal, so you won't want to do much more than ->fetch($uid, "rfc822").
  • sub β€” whenever we parse an imap response and encounter a literal, call this sub, and if it returns a filehandle, read into that filehandle, otherwise read into a scalar in memory.

If a sub is used, when it's called it's passed two arguments:

  • bytes β€” the size of the literal in bytes.
  • previous atom β€” if parsing a list response, the previous atom that was parsed. useful when parsing "fetch" responses since you get key/value type list in the fetch response.

Examples:

# Read rfc822 text of message 3 into file
# (note that the file will have /r/n line terminators)
open(my $fh, ">messagebody.txt");
$IMAP->literal_handle_control($fh);
$IMAP->fetch(3, 'rfc822');
$IMAP->literal_handle_control(0);

# Read the full rfc822 content into a filehandle, but the headers into a scalar
$IMAP->literal_handle_control(sub { lc $_[1] eq 'rfc822' ? scalar File::Temp::tempfile() : undef });
my $res = $IMAP->fetch(3, [ qw(rfc822 rfc822.header) ]);
$IMAP->literal_handle_control(0);
my $filehandle = $res->{3}->{rfc822};
my $hdrstxt = $res->{3}->{'rfc822.header'};
  • release_socket($Close) β€” releases IMAPTalk's ownership of the current socket it's using so it doesn't get disconnected on DESTROY. this returns the socket, and makes sure the IMAPTalk object doesn't hold a reference to it anymore and the connection state is set to "Unconnected". 🫑

Which means β€” and I need you to hear me on this β€” you can't call any methods on the IMAPTalk object anymore. she's gone, bestie. 😭

If the socket is being released and being closed, then $Close is set to true.

  • get_last_error() β€” returns a text string describing the last error that occurred. the receipts. 🧾
  • get_last_completion_response() β€” returns the last completion response to the tagged command.

This is either the string "ok", "no" or "bad" (always lower case). short king energy. πŸ‘‘

  • get_response_code($Response) β€” returns the extra response data generated by a previous call. this is most often used after calling select, which usually generates some set of the following sub-results:
    • permanentflags β€” array reference of flags which are stored permanently.
    • uidvalidity β€” whether the current UID set is valid. see the IMAP RFC for more information on this. if this value changes, then all UIDs in the folder have been changed. 🚨
    • uidnext β€” the next UID number that will be assigned.
    • exists β€” number of messages that exist in the folder.
    • recent β€” number of messages that are recent in the folder.

Other possible responses are alert, newname, parse, trycreate, appenduid, etc.

The values are stored in a hash keyed on the $Response item. they're kept until either they get overwritten by a future response, or explicitly cleared via clear_response_code(). no spontaneous vanishing here. πŸ’…

Examples:

# Select inbox and get list of permanent flags, uidnext and number
#  of message in the folder
$IMAP->select('inbox');
my $NMessages = $IMAP->get_response_code('exists');
my $PermanentFlags = $IMAP->get_response_code('permanentflags');
my $UidNext = $IMAP->get_response_code('uidnext');
  • clear_response_code($Response) β€” clears any response code information. response code information is not normally cleared between calls, so this is your little reset button. 🧼
  • parse_mode(ParseOption => $ParseMode) β€” changes how results of fetch commands are parsed. available options are:
    • BodyStructure β€” parse bodystructure into a more Perl-friendly structure. see the FETCH RESULTS section. πŸ¦‹
    • Envelope β€” parse envelopes into a more Perl-friendly structure. see the FETCH RESULTS section.
    • Annotation β€” parse annotation (from RFC 5257) into a more Perl-friendly structure. see the FETCH RESULTS section.
    • EnvelopeRaw β€” if parsing envelopes, create To/Cc/Bcc and Raw-To/Raw-Cc/Raw-Bcc entries which are array refs of 4 entries each as returned by the IMAP server.
    • DecodeUTF8 β€” if parsing envelopes, decode any MIME encoded headers into Perl UTF-8 strings.

For this to work, you must have 'used' Mail::IMAPTalk with:

use Mail::IMAPTalk qw(:utf8support ...)
  • set_tracing($Tracer) β€” lets you trace both the IMAP input and output sent to the server and returned from the server. super useful for debugging. it returns the previous value of the tracer and then sets it to the value you passed in. possible values for $Tracer are:
    • 0 β€” disable all tracing.
    • 1 β€” print to STDERR.
    • Code ref β€” call the code ref for each line input and output. pass line as parameter.
    • Glob ref β€” print to glob.
    • Scalar ref β€” appends to the referenced scalar.
Note: literals are never passed to the tracer. πŸ™ˆ
  • set_unicode_folders($Unicode) β€” $Unicode should be 1 or 0.

Sets whether folder names are expected and returned as perl unicode strings.

The default is currently 0, BUT YOU SHOULD NOT ASSUME THIS, because it will probably change in the future. the future is coming, bestie. ⏳

If you want to work with perl unicode strings for folder names, you should call

$ImapTalk->set_unicode_folders(1)

and IMAPTalk will automatically encode the unicode strings into IMAP-UTF7 when sending to the IMAP server, and will also decode IMAP-UTF7 back into perl unicode strings when returning results from the IMAP server. it's giving translator-friend energy. 🌈

If you want to work with folder names in IMAP-UTF7 bytes, then call

$ImapTalk->set_unicode_folders(0)

and IMAPTalk will leave folder names as bytes when sending to and returning results from the IMAP server.

  • get_select_state() β€” returns an opaque value that represents the current selected state and folder. you can call select, unselect, etc and then later call set_select_state(...) to return to the previous selected state. save point unlocked. πŸ’Ύ
  • set_select_state($state) β€” restores selected state to a value returned from get_select_state. rewind time, but make it legal. ✨

πŸ“ IMAP FOLDER COMMAND METHODS πŸ“

Note: in all cases where a folder name is used, the folder name is first manipulated according to the current root folder prefix as described in set_root_folder(). don't forget it, it's literally doing work in the background. 🫢
  • select($FolderName, @Opts) β€” performs the standard IMAP 'select' command to select a folder for retrieving/moving/adding messages. if $Opts{ReadOnly} is true, the IMAP EXAMINE verb is used instead of SELECT. choosing violence… politely. πŸ’…

Mail::IMAPTalk will cache the currently selected folder, and if you issue another ->select("XYZ") for the folder that is already selected, it will just return immediately. This can confuse code that expects to get side effects of a select call. for that case, call ->unselect (or ->unselect_or_close) first, then ->select. πŸ™ˆ

unselect()

Runs the plain ol' standard IMAP unselect command. That's it, that's the whole thing. πŸ’…

unselect_or_close()

Does the IMAP unselect command if the server actually has that capability in its list, and if it doesn't? Falls back to the close command instead. Server-dependent behavior, bestie β€” it picks whichever one your server can handle. ✨

examine($FolderName)

Runs the standard IMAP examine command to select a folder in read only mode for retrieving messages. Literally the exact same thing as calling select($FolderName, 1). Peep select() for the deets. πŸ‘€

select_with_state($FolderName)

Runs the standard IMAP select command to select a folder. When it works you get back a hashref of uidnext, uidvalidity and messagecount; when it doesn't, you get an undef. No in-between, it's giving all or nothing. πŸŽ€

create($FolderName)

Runs the standard IMAP create command to create a new folder. New folder, who dis. ✨

delete($FolderName)

Runs the standard IMAP delete command to delete a folder. Gone. Poof. Bye. πŸ’€

localdelete($FolderName)

Runs the IMAP localdelete command to delete a folder β€” and the way that it doesn't delete subfolders, even of INBOX, and it's always immediate. No takebacks, no chill. 🫢

rename($OldFolderName, $NewFolderName)

Runs the standard IMAP rename command to rename a folder. Glow-up for your mailbox name. πŸ‘‘

list($Reference, $Name)

Runs the standard IMAP list command and hands you back a list of the available folders. Window shopping, but for mailboxes. 🌸

xlist($Reference, $Name)

Runs the IMAP xlist extension command β€” same vibe as list but it also returns the available folders and their special-use attributes. Extra credit info, iykyk. 🌈

id($key = $value, ...)

Runs the IMAP extension command id. ✨

lsub($Reference, $Name)

Runs the standard IMAP lsub command and returns a list of your subscribed folders. Only the ones you actually committed to. πŸ’–

subscribe($FolderName)

Runs the standard IMAP subscribe command to subscribe to a folder. Hitting the follow button, basically. πŸ”₯

unsubscribe($FolderName)

Runs the standard IMAP unsubscribe command to unsubscribe from a folder. Unfollowing, but make it mail. πŸ™ˆ

check()

Runs the standard IMAP check command to checkpoint the current folder. Lil save point. πŸ’Ύ

setacl($FolderName, $User, $Rights)

Runs the IMAP setacl command to set the access control list details of a folder/mailbox. Sling RFC 4314 for the full tea on the IMAP ACL extension.

$User is the username you're setting the access rights for. $Rights is either a list of absolute rights to set, OR a list prefixed by - to remove those rights, OR prefixed by + to add those rights. Choose your fighter. πŸŽ€

The rights, bestie, and what they actually mean:

  • l β€” lookup (mailbox is visible to LIST/LSUB commands)
  • r β€” read (SELECT the mailbox, perform CHECK, FETCH, PARTIAL, SEARCH, COPY from mailbox)
  • s β€” keep seen/unseen information across sessions (STORE SEEN flag)
  • w β€” write (STORE flags other than SEEN and DELETED)
  • i β€” insert (perform APPEND, COPY into mailbox)
  • p β€” post (send mail to submission address for mailbox, not enforced by IMAP4 itself)
  • k β€” create mailboxes (CREATE new sub-mailboxes in any implementation-defined hierarchy, parent mailbox for the new mailbox name in RENAME)
  • x β€” delete mailbox (DELETE mailbox, old mailbox name in RENAME)
  • t β€” delete messages (set or clear \DELETED flag via STORE, set \DELETED flag during APPEND/COPY)
  • e β€” perform EXPUNGE and expunge as a part of CLOSE
  • a β€” administer (perform SETACL)
⚠️ drama alert: because RFC 2086 is ambiguous, some existing RFC 2086 server implementations use the c right to control the DELETE command, while others chose to use the d right for it. Check out "2.1.1. Obsolete Rights" in RFC 4314 for the full breakdown.
  • c β€” create (CREATE new sub-mailboxes in any implementation-defined hierarchy)
  • d β€” delete (STORE DELETED flag, perform EXPUNGE)

The standard access control configurations for cyrus are:

read   = "lrs"
post   = "lrsp"
append = "lrsip"
write  = "lrswipcd"
all    = "lrswipcda"

Examples:

# Get full access for user 'joe' on his own folder
$IMAP->setacl('user.joe', 'joe', 'lrswipcda') || die "IMAP error: $@";
# Remove write, insert, post, create, delete access for user 'andrew'
$IMAP->setacl('user.joe', 'andrew', '-wipcd') || die "IMAP error: $@";
# Add lookup, read, keep unseen information for user 'paul'
$IMAP->setacl('user.joe', 'paul', '+lrs') || die "IMAP error: $@";

getacl($FolderName)

Runs the IMAP getacl command to get the access control list details of a folder/mailbox. Sling RFC 4314 for the full tea on the IMAP ACL extension. Returns an array of pairs β€” each pair is a username followed by the access rights for that user. Check setacl for what those rights mean. 🌸

Examples:

my $Rights = $IMAP->getacl('user.joe') || die "IMAP error : $@";
$Rights = [
  'joe', 'lrs',
  'andrew', 'lrswipcda'
];

$IMAP->setacl('user.joe', 'joe', 'lrswipcda') || die "IMAP error : $@";
$IMAP->setacl('user.joe', 'andrew', '-wipcd') || die "IMAP error : $@";
$IMAP->setacl('user.joe', 'paul', '+lrs') || die "IMAP error : $@";

$Rights = $IMAP->getacl('user.joe') || die "IMAP error : $@";
$Rights = [
  'joe', 'lrswipcd',
  'andrew', 'lrs',
  'paul', 'lrs'
];

deleteacl($FolderName, $Username)

Runs the IMAP deleteacl command to nuke all access control information for the given user on the given folder. Check setacl for what those rights mean. πŸ’…

Examples:

my $Rights = $IMAP->getacl('user.joe') || die "IMAP error : $@";
$Rights = [
  'joe', 'lrswipcd',
  'andrew', 'lrs',
  'paul', 'lrs'
];

# Delete access information for user 'andrew'
$IMAP->deleteacl('user.joe', 'andrew') || die "IMAP error : $@";

$Rights = $IMAP->getacl('user.joe') || die "IMAP error : $@";
$Rights = [
  'joe', 'lrswipcd',
  'paul', 'lrs'
];

setquota($FolderName, $QuotaDetails)

Runs the IMAP setquota command to set the usage quota details of a folder/mailbox. Sling RFC 2087 for the deets on the IMAP quota extension. $QuotaDetails is a bracketed list of limit item/value pairs, which represent a particular type of limit and the value to set it to.

Current limits are:

  • STORAGE β€” Sum of messages' RFC822.SIZE, in units of 1024 octets
  • MESSAGE β€” Number of messages

Examples:

# Set maximum size of folder to 50M and 1000 messages
$IMAP->setquota('user.joe', '(storage 50000)') || die "IMAP error: $@";
$IMAP->setquota('user.john', '(messages 1000)') || die "IMAP error: $@";
# Remove quotas
$IMAP->setquota('user.joe', '()') || die "IMAP error: $@";

getquota($FolderName)

Runs the standard IMAP getquota command to get the quota details of a folder/mailbox. Sling RFC 2087 for the deets on the IMAP quota extension. Returns an array reference to quota limit triplets. Each triplet is made of: limit item, current value, maximum value. 🧚

Heads up bestie: this only returns the quota for a folder if it's actually had a quota set on it. It's possible a parent folder has a quota too, which affects sub-folders. Use getquotaroot to find out if that's the case. πŸ’–

Examples:

my $Result = $IMAP->getquota('user.joe') || die "IMAP error: $@";
$Result = [
  'STORAGE', 31, 50000,
  'MESSAGE', 5, 1000
];

getquotaroot($FolderName)

Runs the IMAP getquotaroot command to get the quota details of a folder/mailbox and the possible root quota too. Sling RFC 2087 for the deets on the IMAP quota extension.

Ngl the result of this one is a little complex β€” unfortunately it doesn't map super easily into any structure, because there are several different responses.

Basically it's a hash reference. The 'quotaroot' item is the response that lists the root quotas applying to the given folder. The first item is the folder name, and the remaining items are the quota root items. There's then a hash item for each quota root item. Honestly it's easiest to just look at the example below. πŸ‘€

Examples:

my $Result = $IMAP->getquotaroot('user.joe.blah') || die "IMAP error: $@";
$Result = {
  'quotaroot' => [
    'user.joe.blah', 'user.joe', ''
  ],
  'user.joe' => [
    'STORAGE', 31, 50000,
    'MESSAGES', 5, 1000
  ],
  '' => [
    'MESSAGES', 3498, 100000
  ]
};

message_count($FolderName)

Returns the number of messages in a folder. Wanna know more about what's in there? Peep status() for extra info about the messages in a folder. ✨

status($FolderName, $StatusList)

Runs the standard IMAP status command to retrieve status information about a folder/mailbox.

$StatusList is a bracketed list of folder items to get the status of. It can contain: messages, recent, uidnext, uidvalidity, unseen.

The return value is a hash reference of lc(status-item) => value. πŸŽ€

Examples:

my $Res = $IMAP->status('inbox', '(MESSAGES UNSEEN)');

$Res = {
  'messages' => 8,
  'unseen' => 2
};

multistatus($StatusList, @FolderNames)

Performs many IMAP status commands across a whole list of folders. It fires all the commands off at once and waits for the responses, which is how it speeds up latency issues. Efficiency queen. πŸ‘‘

Returns a hash ref of folder name => status results.

If an error occurs, the annotation result is a scalar ref to the completion response string (eg 'bad', 'no', etc). 😭

getannotation($FolderName, $Entry, $Attribute)

Runs the IMAP getannotation command to get the annotation(s) for a mailbox. Sling the imap-annotatemore extension for the deets. 🌈

Examples:

my $Result = $IMAP->getannotation('user.joe.blah', '/*' '*') || die "IMAP error: $@";
$Result = {
  'user.joe.blah' => {
    '/vendor/cmu/cyrus-imapd/size' => {
      'size.shared' => '5',
      'content-type.shared' => 'text/plain',
      'value.shared' => '19261'
    },
    '/vendor/cmu/cyrus-imapd/lastupdate' => {
      'size.shared' => '26',
      'content-type.shared' => 'text/plain',
      'value.shared' => '26-Mar-2004 13:31:56 -0800'
    },
    '/vendor/cmu/cyrus-imapd/partition' => {
      'size.shared' => '7',
      'content-type.shared' => 'text/plain',
      'value.shared' => 'default'
    }
  }
};

getmetadata($FolderName, [ \%Options ], @Entries)

Runs the IMAP getmetadata command to get the metadata items for a mailbox. Sling RFC 5464 for the deets. πŸ“–

If $Options is passed, it's a hashref of options you wanna set. πŸŽ€

And if foldername is the empty string '', you get the server annotations instead of some folder's. Sneaky but useful.

Examples, bestie, watch closely:

my $Result = $IMAP->getmetadata('user.joe.blah', {depth => 'infinity'}, '/shared') || die "IMAP error: $@";
$Result = {
  'user.joe.blah' => {
    '/shared/vendor/cmu/cyrus-imapd/size' => '19261',
    '/shared/vendor/cmu/cyrus-imapd/lastupdate' => '26-Mar-2004 13:31:56 -0800',
    '/shared/vendor/cmu/cyrus-imapd/partition' => 'default',
  }
};
my $Result = $IMAP->getmetadata('', "/shared/comment");
$Result => {
  '' => {
    '/shared/comment' => "Shared comment",
  }
};

multigetmetadata(@Entries, @FolderNames)

Bestie this one is the ✨batch✨ version. It runs many IMAP 'getmetadata' commands across a whole list of folders β€” sends all of them at once and then waits for the responses. The whole point? It speeds up latency issues. No more waiting around one by one like it's 2004. πŸ’…

Returns a hash ref of folder name => metadata results.

If an error happens, the annotation result is a scalar ref to the completion response string (like 'bad', 'no', etc). So heads up, that slot isn't always shaped like the rest. 🫢

setannotation($FolderName, $Entry, [ $Attribute, $Value ])

Runs the IMAP 'setannotation' command to get the annotation(s) for a mailbox. Go read the imap-annotatemore extension for the deets, iykyk. πŸ“Ž

Examples:

my $Result = $IMAP->setannotation('user.joe.blah', '/comment', [ 'value.priv' 'A comment' ])
  || die "IMAP error: $@";

setmetadata($FolderName, $Name, $Value, $Name2, $Value2)

Runs the IMAP 'setmetadata' command. See RFC 5464 for the full tea. 🍡

Examples:

my $Result = $IMAP->setmetadata('user.joe.blah', '/comment', 'A comment')
  || die "IMAP error: $@";

close()

Runs the standard IMAP 'close' command β€” it expunges the deleted messages from the current folder and takes you back to the Authenticated state. Very "and we're done here, bye." πŸ¦‹

Heads up: expunged means gone gone, okay? That's your deleted messages, actually deleted, fr fr. 😭

idle(&Callback, [ $Timeout ])

Performs an IMAP idle call. It calls the callback you give it for each IDLE event it receives. πŸ’–

How the callback talks to idle:

  • If the callback returns 0, the idle keeps going.
  • If the callback returns 1, the idle is finished and this call returns.

If no timeout is passed, it'll just keep idling until your callback returns 1 or the server disconnects. Committed. Loyal. πŸ‘‘

If a timeout is passed (and yes, a 0 timeout counts), the call will return if no events show up within that time. It returns the result of the DONE command, and sets $Self->get_response_code('timeout') to true. ✨

If the server closes the connection with a "bye" response, it returns undef and $@ =~ /bye/ will be true, with the rest of the bye line following after it. Rude of the server, but at least you'll know.


πŸ’Œ IMAP MESSAGE COMMAND METHODS πŸ’Œ

fetch([ %ParseMode ], $MessageIds, $MessageItems)

Runs the standard IMAP 'fetch' command to retrieve the specified message items from the specified message IDs. This is the big one, bestie. πŸŽ€

That first parameter can be an optional hash reference that overrides particular parse mode parameters just for this fetch. See "parse_mode" for possible keys. 🌸

$MessageIds can be one of two forms:

  1. A text string with a comma separated list of message IDs or message ranges separated by colons. A '*' represents the highest message number.

Examples:

  • '1' - first message
  • '1,2,5'
  • '1:*' - all messages
  • '1,3:*' - all but message 2

Note that , separated lists and : separated ranges can be mixed, but to make sure a certain hack works, if a '*' is used, it must be the last character in the string. No cap. 🚨

  1. An array reference with a list of message IDs or ranges. The array contents are "join(',', ...)"ed together.
Note: If the uid() state has been set to true, then all message IDs must be message UIDs. Don't mix it up, bestie. πŸ™ˆ

$MessageItems can be one of, or a bracketed list of:

  • uid
  • flags
  • internaldate
  • envelope
  • bodystructure
  • body
  • body[section]<partial>
  • body.peek[section]<partial>
  • rfc822
  • rfc822.header
  • rfc822.size
  • rfc822.text
  • fast
  • all
  • full

It would be a good idea to see RFC 3501 for what all these mean. Not me just trusting you to read the RFC. πŸ“–

Examples:

my $Res = $IMAP->fetch('1:*', 'rfc822.size');
my $Res = $IMAP->fetch([1,2,3], '(bodystructure envelope)');

Return results:

The results the IMAP server sends back get parsed into a Perl structure. See the section FETCH RESULTS for all the interesting details. πŸ‘‘

Note that messages can disappear on you β€” so you may not get back all the entries you expect in the hash. Ghosting, but from the server. πŸ’€

There is one piece of magic. If your request is for a single uid, (eg "123"), and no data is returned, we return undef, because it's easier to handle as an error condition. That's it, that's the magic. ✨

copy($MsgIds, $ToFolder)

Runs the standard IMAP copy command to copy a set of messages from one folder to another. Simple, clean, slay. πŸ’…

append($FolderName, optional $MsgFlags, optional $MsgDate, $MessageData)

Runs the standard IMAP append command to append a new message into a folder. πŸŽ€

The $MessageData you wanna append can either be a Perl scalar holding the data, or a file handle to read the data from. In each case, the data must be in proper RFC 822 format with \r\n line terminators. Do not skip the \r\n, I'm begging. 😭

Any optional fields you don't need should be removed, not left blank. Blank β‰  absent, bestie.

Examples:

# msg.txt should have \r\n line terminators
open(F, "msg.txt");
$IMAP->append('inbox', \*F);

my $MsgTxt =<<MSG;
From: blah\@xyz.com
To: whoever\@whereever.com
...
MSG

$MsgTxt =~ s/\n/\015\012/g;
$IMAP->append('inbox', { Literal => $MsgTxt });

search($MsgIdSet, @SearchCriteria)

Runs the standard IMAP search command. The result is an array reference to a list of message IDs (or UIDs if you're in Uid mode) of the messages that are in $MsgIdSet and also meet the search criteria. πŸ”

@SearchCriteria is a list of search specifications. For example, to look for ASCII messages bigger than 2000 bytes you'd set the list to be:

my @SearchCriteria = ('CHARSET', 'US-ASCII', 'LARGER', '2000');

Examples:

my $Res = $IMAP->search('1:*', 'NOT', 'DELETED');
$Res = [ 1, 2, 5 ];

store($MsgIdSet, $FlagOperation, $Flags)

Runs the standard IMAP store command. It changes the flags associated with a set of messages. 🏷️

Examples:

$IMAP->store('1:*', '+flags', '(\\deleted)');
$IMAP->store('1:*', '-flags.silent', '(\\read)');

expunge()

Runs the standard IMAP expunge command. This actually deletes any messages marked as deleted. Like, for real for real. No undo. 🌈

Bestie, I'm serious: "marked as deleted" becomes "deleted" right here. Blink and they're gone. πŸ’€

uidexpunge($MsgIdSet)

Runs the IMAP uid expunge command as per RFC 2359. Fancy. ✨

sort($SortField, $CharSet, @SearchCriteria)

Runs the extension IMAP sort command. The result is an array reference to a list of message IDs (or UIDs if in Uid mode) in sorted order. So satisfying when it lines up. πŸ’–

It would probably be a good idea to look at the sort RFC 5256 details at somewhere like: http://www.ietf.org/rfc/rfc5256.txt

Examples:

my $Res = $IMAP->sort('(subject)', 'US-ASCII', 'NOT', 'DELETED');
$Res = [ 5, 2, 3, 1, 4 ];

thread($ThreadType, $CharSet, @SearchCriteria)

Runs the extension IMAP thread command. $ThreadType should be one of 'REFERENCES' or 'ORDEREDSUBJECT'. You should check the capability() of the server to see if it supports one or both of these. Don't just assume, bestie β€” vibe check the server first. 🧚

Examples

my $Res = $IMAP->thread('REFERENCES', 'US-ASCII', 'NOT', 'DELETED');
$Res = [ [10, 15, 20], [11], [ [ 12, 16 ], [13, 17] ];

fetch_flags($MessageIds)

Runs an IMAP 'fetch flags' command to retrieve the specified flags for the specified messages. 🚩

This is just a special fast path version of "fetch". Same energy, zoomed in.

fetch_meta($MessageIds, @MetaItems)

Runs an IMAP 'fetch' command to retrieve the specified meta items. These must be simple items that return only atoms (eg no flags, bodystructure, body, envelope, etc). Keep it lowkey, okay? 🌸

This is just a special fast path version of "fetch".


πŸ‘‘ IMAP CYRUS EXTENSION METHODS πŸ‘‘

Methods provided by extensions to the cyrus IMAP server. Cyrus staying booked and busy, we love to see it. πŸ’…

Note: In all cases where a folder name is used, the folder name is first manipulated according to the current root folder prefix as described in set_root_folder(). So go read that one if this surprises you. πŸŽ€

xrunannotator($MessageIds)

Run the xannotator command on the given message id's. That's the whole thing. Short, sweet, iconic. ✨

πŸŽ€ IMAP HELPER FUNCTIONS πŸŽ€

These are the little besties that do the fiddly work for you once you've already pulled down a bodystructure. No cap, they save you so much pain.


get_body_part($BodyStruct, $PartNum)

Okay so picture this: you fetched a bodystructure, and now you want one specific little sub-part out of it. This helper does exactly that πŸ’…. You hand it a top-level body structure plus a part number, and it hands you back a reference to the bodystructure sub-part that that part number points at.

Examples, because seeing it is believing:

# Fetch body structure
my $FR = $IMAP->fetch(1, 'bodystructure');
my $BS = $FR->{1}->{bodystructure};

# Parse further to find particular sub part
my $P12 = $IMAP->get_body_part($BS, '1.2');
$P12->{'IMAP->Partnum'} eq '1.2' || die "Unexpected IMAP part number";

find_message($BodyStruct)

Another glow-up helper for parsing fetched bodystructures ✨. You give it a bodystructure, it gives you back a hash reference, and inside that hash lives all of this:

  text => $best_text_part
  html => $best_html_part (optional)
  textlist => [ ... text/html (if no alt text bits)/image (if inline) parts ... ]
  htmllist => [ ... text (if no alt html bits)/html/image (if inline) parts ... ]
  att => [ {
     bs => $part, text => 0/1, html => 0/1, msg => 1/0,
   }, { ... }, ... ]

Okay so like, say you've got a message with text and html pages, and then some list software manager came along and stapled a header/footer onto it. It ends up looking like this whole situation:

  multipart/mixed
    text/plain, cd=inline - A
    multipart/mixed
      multipart/alternative
        multipart/mixed
          text/plain, cd=inline - B
          image/jpeg, cd=inline - C
          text/plain, cd=inline - D
        multipart/related
          text/html - E
          image/jpeg - F
      image/jpeg, cd=attachment - G
      application/x-excel - H
      message/rfc822 - J
    text/plain, cd=inline - K

In that case, the list items you'd get back are:

  text => B
  html => E
  textlist => [ A, B, C, D, K ]
  htmllist => [ A, E, K ]
  att => [
    { bs => C, text => 1, html => 1 },
    { bs => F, text => 1, html => 0 },
    { bs => G, text => 1, html => 1 },
    { bs => H, text => 1, html => 1 },
    { bs => J, text => 0, html => 0, msg => 1 },
  ]

Examples, bestie:

# Fetch body structure
my $FR = $IMAP->fetch(1, 'bodystructure');
my $BS = $FR->{1}->{bodystructure};

# Parse further to find message components
my $MC = $IMAP->find_message($BS);
$MC = { 'plain' => ... text body struct ref part ...,
        'html' => ... html body struct ref part (if present) ...
        'htmllist' => [ ... html body struct ref parts (if present) ... ] };

# Now get the text part of the message
my $MT = $IMAP->fetch(1, 'body[' . $MC->{text}->{'IMAP-Part'} . ']');

generate_cid( $Token, $PartBS )

This method generates a ContentID based on $Token and $PartBS.

And the vibe here is determinism. The same value should always be returned for a given $Token and $PartBS, periodt.


build_cid_map($BodyStruct, [ $IMAP, $Uid, $GenCidToken ])

Another helper for digging deeper into fetched bodystructures πŸ¦‹. It recursively parses the bodystructure and returns a hash of Content-ID to bodystruct part references. Super useful when you're trying to figure out CID links from an HTML message.

And here's the tea: if you pass a Mail::IMAPTalk object as the second parameter, the CID map it builds may be even more detailed πŸ‘‘. Turns out some stupid versions of exchange stick the details in the Content-Location header instead of the Content-Type header. If that's the case, this will try and fetch the header from the message.

Examples:

# Fetch body structure
my $FR = $IMAP->fetch(1, 'bodystructure');
my $BS = $FR->{1}->{bodystructure};

# Parse further to get CID links
my $CL = build_cid_map($BS);
$CL = { '2958293123' => ... ref to body part ..., ... };

obliterate($CyrusName)

Give it a username (optionally username\@domain) and it will immediately delete all messages belonging to that user 😭. Uses LOCALDELETE. Quite FastMail Patchd Cyrus specific, iykyk. It's fast, it's brutal, it does not ask twice β€” so pls be careful with this one.


make_sequence_set($MessageIds)

my $SetSeq = $IMAP->make_sequence_set($Ids);

This method returns a reference to a "sequence-set" string suitable for use in IMAP commands πŸ”₯. And the reason it returns a reference instead of a plain string? So the result can be passed directly to IMAP-generating methods and the sequence set won't get quoted πŸ™ˆ. This quoting is especially problematic when quoting a range like "1:*".


🫢 IMAP CALLBACKS 🫢

By default, these methods do nothing. But you can dervice from Mail::IMAPTalk and override these methods to trap any things you want to catch β€” little hooks to do your own thing when stuff happens ✨.

cb_switch_folder($CurrentFolder, $NewFolder)

Called when the currently selected folder is being changed. Like, select is called and it's definitely a different folder being selected, or the unselect methods get called. That kind of moment πŸ’«.

cb_folder_changed($Folder)

Called when a command changes the contents of a folder (eg copy, append, etc). $Folder is the name of the folder that's changing. Not me spying on folder drama πŸ‘€.

πŸ’– FETCH RESULTS πŸ’–

okay bestie, this is the one. the fetch operation is probably the thing you'll be doing constantly with an IMAP connection. it's how you grab info about a message or a whole lil squad of messages β€” header fields, flags, chunks of the message body, all of it. ✨

Mail::IMAPTalk will always parse the results of a fetch call into a Perl-ish structure for you, no cap. though heads up: bodystructure, envelope and uid responses might get extra parsing depending on the parse_mode state and the uid state (more on that below πŸ‘‡).

so like, picture this: here's an example where C is what the client sends and S is what the server claps back with.

C: a100 fetch 5,6 (flags rfc822.size uid)
S: * 1 fetch (UID 1952 FLAGS (\recent \seen) RFC822.SIZE 1150)
S: * 2 fetch (UID 1958 FLAGS (\recent) RFC822.SIZE 110)
S: a100 OK Completed

the fetch command gets sent by just... calling this:

my $Res = $IMAP->fetch('1:*', '(flags rfc822.size uid)');

and the result you get back? iconic. it looks like this:

$Res = {
  1 => {
    'uid' => 1952,
    'flags' => [ '\\recent', '\\seen' ],
    'rfc822.size' => 1150
  },
  2 => {
    'uid' => 1958,
    'flags' => [ '\\recent' ],
    'rfc822.size' => 110
  }
};

a couple things to clock here πŸ”:

  1. the message IDs got turned into a hash going from message ID ➑️ fetch response result.
  2. the response items (like uid, flags, etc) got turned into a hash for each message, and they also got lowercased. cute.
  3. any other bracketed (...) lists became array references. slay.

and ngl, that's basically how all fetch responses get parsed. but there's one major plot twist when the IMAP connection is in uid mode. πŸ‘€ in that case, the message IDs in the main hash become message UIDs instead, and the uid entry inside the inner hash gets yeeted. so the example above would become:

my $Res = $IMAP->fetch('1:*', '(flags rfc822.size)');

$Res = {
  1952 => {
    'flags' => [ '\\recent', '\\seen' ],
    'rfc822.size' => 1150
  },
  1958 => {
    'flags' => [ '\\recent' ],
    'rfc822.size' => 110
  }
};

πŸ¦‹ Bodystructure πŸ¦‹

when we're dealing with messages, we gotta understand the MIME structure of the thing so we can figure out what's the text body, what's an attachment, etc. that's literally where the bodystructure item from an IMAP server comes in. πŸ’…

C: a101 fetch 1 (bodystructure)
S: * 1 fetch (BODYSTRUCTURE ("TEXT" "PLAIN" NIL NIL NIL "QUOTED-PRINTABLE" 255 11 NIL ("INLINE" NIL) NIL))
S: a101 OK Completed

the fetch command gets sent by calling:

my $Res = $IMAP->fetch(1, 'bodystructure');

and yeah, as expected, the resulting response looks like this:

$Res = {
  1 => {
    'bodystructure' => [
      'TEXT', 'PLAIN', undef, undef, undef, 'QUOTED-PRINTABLE',
        255, 11, UNDEF, [ 'INLINE', undef ], undef
    ]
  }
};

but bestie, if you set the parse_mode(BodyStructure => 1), then the result becomes:

$Res = {
  '1' => {
    'bodystructure' => {
      'MIME-Type' => 'text',
      'MIME-Subtype' => 'plain',
      'MIME-TxtType' => 'text/plain',
      'Content-Type' => {},
      'Content-ID' => undef,
      'Content-Description' => undef,
      'Content-Transfer-Encoding' => 'QUOTED-PRINTABLE',
      'Size' => '3569',
      'Lines' => '94',
      'Content-MD5' => undef,
      'Disposition-Type' => 'inline',
      'Content-Disposition' => {},
      'Content-Language' => undef,
      'Remainder' => [],
      'IMAP-Partnum' => ''
    }
  }
};

a couple points to note here πŸ“:

  1. all the positional fields from the bodystructure list response got turned into nicely named key/value hash items. glow up.
  2. the MIME-Type and MIME-Subtype fields got lowercased.
  3. an IMAP-Partnum item got added. you can pass that field's value as the section number of an IMAP body fetch call to retrieve the text of that IMAP section. v useful.

so in general, these items are defined for all body structures:

  • MIME-Type
  • MIME-Subtype
  • Content-Type
  • Disposition-Type
  • Content-Disposition
  • Content-Language

for all bodystructures EXCEPT the ones that have a MIME-Type of multipart, these are defined:

  • Content-ID
  • Content-Description
  • Content-Transfer-Encoding
  • Size
  • Content-MD5
  • Remainder
  • IMAP-Partnum

for bodystructures where MIME-Type is text, there's an extra item called Lines. periodt.

for bodystructures where MIME-Type is message and MIME-Subtype is rfc822, the extra items Message-Envelope, Message-Bodystructure and Message-Lines are defined. the Message-Bodystructure item is itself a reference to a whole entire bodystructure hash with all the format info of the contained message. and Message-Envelope is a hash structure holding the message header info β€” peep the Envelope entry below πŸ‘‡

for bodystructures where MIME-Type is multipart, an extra item called MIME-Subparts is defined. that MIME-Subparts item is an array reference, and each item in it is a reference to an entire bodystructure hash with all the format info of each MIME sub-part. yes, it's hashes in arrays in hashes, we love a nesting moment πŸͺ†

if you need to keep processing, you can use the find_message() function β€” it'll analyse the body structure and find which part corresponds to the main text/html message parts worth displaying. you can also use find_cid_parts() to find CID links in an html message. 🫢


πŸ’Œ Envelope πŸ’Œ

the envelope structure has most of the addressing header fields from an email message. here's an example envelope fetch (the IMAP server's response has been tidied up here for your viewing pleasure):

C: a102 fetch 1 (envelope)
S: * 1 FETCH (ENVELOPE
    ("Tue, 7 Nov 2000 08:31:21 UT"      # Date
     "FW: another question"             # Subject
     (("John B" NIL "jb" "abc.com"))    # From
     (("John B" NIL "jb" "abc.com"))    # Sender
     (("John B" NIL "jb" "abc.com"))    # Reply-To
     (("Bob H" NIL "bh" "xyz.com")      # To
      ("K Jones" NIL "kj" "lmn.com"))
     NIL                                # Cc
     NIL                                # Bcc
     NIL                                # In-Reply-To
     NIL)                               # Message-ID
   )
S: a102 OK Completed

the fetch command gets sent by calling:

my $Res = $IMAP->fetch(1, 'envelope');

and you already get the idea of what the resultant response would be, i fear. πŸ™ˆ again, if you change parse_mode(Envelope => 1), you get a neat structure like this:

$Res = {
  '1' => {
    'envelope' => {
      'Date' => 'Tue, 7 Nov 2000 08:31:21 UT',
      'Subject' => 'FW: another question',
      'From' => '"John B" <jb@abc.com>',
      'Sender' => '"John B" <jb@abc.com>',
      'Reply-To' => '"John B" <jb@abc.com>',
      'To' => '"Bob H" <bh@xyz.com>, "K Jones" <kj@lmn.com>',
      'Cc' => '',
      'Bcc' => '',
      'In-Reply-To' => undef,
      'Message-ID' => undef,

      'From-Raw' => [ [ 'John B', undef, 'jb', 'abc.com' ] ],
      'Sender-Raw' => [ [ 'John B', undef, 'jb', 'abc.com' ] ],
      'Reply-To-Raw' => [ [ 'John B', undef, 'jb', 'abc.com' ] ],
      'To-Raw' => [
        [ 'Bob H', undef, 'bh', 'xyz.com' ],
        [ 'K Jones', undef, 'kj', 'lmn.com' ],
      ],
      'Cc-Raw' => [],
      'Bcc-Raw' => [],
    }
  }
};

all the fields here come straight from the email headers, fr fr. see RFC 822 for more deets. πŸ“–


🌸 Annotation 🌸

if the server supports RFC 5257 (the ANNOTATE Extension), then you can fetch per-message annotations. 🀩

annotation responses would normally come back as a nested set of arrays. but it's so much easier to access the results as a nested set of hashes, so the results get converted like that when the Annotation parse mode is enabled β€” and bestie, that's on by default. king behavior πŸ‘‘

here's part of an example straight from the RFC:

S: * 12 FETCH (UID 1123 ANNOTATION
   (/comment (value.priv "My comment"
      size.priv "10")
   /altsubject (value.priv "Rhinoceroses!"
      size.priv "13")

so the fetch command:

my $Res = $IMAP->fetch(1123, 'annotation', [ '/*', [ 'value.priv', 'size.priv' ] ]);

would have the result:

$Res = {
  '1123' => {
    'annotation' => {
      '/comment' => {
        'value.priv' => 'My comment',
        'size.priv => 10
      },
      '/altsubject' => {
        'value.priv' => '"Rhinoceroses',
        'size.priv => 13
      }
    }
  }
}

🫧 INTERNAL METHODS 🫧

bestie these are the ✨backstage✨ methods, the ones doing all the heavy lifting in the wings. You probably don't need to call these yourself, but like... here's what they're up to πŸ’…
  • _imap_cmd($Command, $IsUidCmd, $RespItems, @Args) β€” runs a standard IMAP command, no biggie, just the whole engine of this module fr fr.

πŸŽ€ Method arguments

  • $Command β€” the text string of the command you're throwing at the IMAP server (think 'select', 'search', etc).
  • $IsUidCmd β€” 1 if this command is about message ids and can get the UID prefix slapped on it, 0 if not.
  • $RespItems β€” the responses you're hunting for from the command (like 'list', 'fetch', etc). Commands that return results usually hand them back untagged, and here's the vibe in action:
C123 uid fetch 1:* (flags)
* 1 FETCH (FLAGS (\Seen) UID 1)
* 2 FETCH (FLAGS (\Seen) UID 2)
C123 OK Completed

So between firing off the command and seeing the 'OK Completed' response, we have to scoop up all those untagged 'FETCH' response items β€” which is exactly why you'd pass 'fetch' (always lowercase, always πŸ’€) as $RespItems to extract them.

And plot twist: this can also be a hash ref of callback functions! Peep _parse_response for more examples πŸ‘€

  • @Args β€” literally any extra arguments you wanna pass to the command. Go wild.

  • _send_cmd($Self, $Cmd, @InArgs) β€” helper method that _imap_cmd leans on to actually build (and quote, where quoting is needed) the command arguments, then yeet the actual command out there.
  • _send_data($Self, $Opts, $Buffer, @Args) β€” another helper used by _send_cmd, doing the exact same job of building (and quoting where necessary) command arguments and sending the actual command.
  • _parse_response($Self, $RespItems, [ \%ParseMode ]) β€” the helper _imap_cmd calls after the command goes out. This one grabs data from the IMAP socket, parses it into Perl structures, and hands you back the results. πŸ’–

$RespItems is either a string β€” the untagged response(s) to find and return β€” or, for custom processing, a hash ref.

If it's a hash ref, every key is an untagged response to look out for, and every value is a callback function fired for that corresponding untagged response.

Each callback gets called with 2 or 3 arguments: the untagged response string, the remainder of the line parsed into an array ref, and β€” for fetch type responses β€” the id gets passed as that third argument. 🎁

One more piece of magic: if you pass a 'responseitem' key, the value has to be a string, and that string is the untagged response returned from the function. No cap.

  • _require_capability($Self, $Capability) β€” helper that checks the server actually has a certain capability. If it doesn't? It sets the internal last error, $@, and returns undef. Rude but fair 😭
  • _trace($Self, $Line) β€” helper that outputs any tracing data. Just tracing things, being nosy πŸ•΅οΈ
  • _is_current_folder($Self, $FolderName) β€” returns true if a folder is currently selected and that folder is $FolderName. Simple, iconic, does what it says.

πŸ”Œ INTERNAL SOCKET FUNCTIONS πŸ”Œ

  • _next_atom($Self) β€” returns the next atom from the current line. It uses $Self->{ReadLine} for line data, or if that's undef, it fills it with a fresh new line of data straight from the IMAP connection socket and then starts processing. ✨

So if the next atom is:

  • An unquoted string β€” it just returns the string, easy peasy.
  • A quoted string β€” it unquotes it, swaps any \" occurrences into ", and returns the string.
  • A literal (like {NBytes}\r\n) β€” it reads the number of bytes of data in the literal into a scalar or file (depends on "literal_handle_control" btw, iykyk).
  • A bracketed structure β€” it reads all the sub-atoms inside the structure and returns an array reference with all of them.

In every single case, after parsing the atom it strips any trailing space separator, then puts the remainder of the line back into $Self->{ReadLine} so it's ready for the next _next_atom() call. Recycling queen ♻️

  • _next_simple_atom($Self) β€” the faster version of _next_atom() for known simple cases. Speedrun edition πŸƒβ™€οΈ
  • _remaining_atoms($Self) β€” returns all the remaining atoms for the current line in the read line buffer as an array reference. Leaves $Self->{ReadLine} eq ''. Check _next_atom() for the deets.
  • _remaining_line($Self) β€” returns the remaining data in the read line buffer ($Self->{ReadLine}) as a scalar string/data value.
  • _fill_imap_read_buffer($Self) β€” waits until data is available on the IMAP connection socket (or a timeout happens, whichever comes first πŸ™ˆ). Reads the data into the internal buffer $Self->{ReadBuf}. After that you can use _imap_socket_read_line(), _imap_socket_read_bytes() or _copy_imap_socket_to_handle() to read data from the buffer in lines or bytes at a time.
  • _imap_socket_read_line($Self) β€” reads a \r\n terminated list from the buffered IMAP connection socket.
  • _imap_socket_read_bytes($Self, $NBytes) β€” reads a certain number of bytes from the buffered IMAP connection socket.
  • _imap_socket_out($Self, $Data) β€” writes the data in $Data to the IMAP connection socket. Sending it fr πŸ’Œ
  • _copy_handle_to_imapsocket($Self, $InHandle) β€” copies a given number of bytes from a file handle to the IMAP connection.
  • _copy_imap_socket_to_handle($Self, $OutHandle, $NBytes) β€” copies data from the IMAP socket to a file handle. This one's different from _copy_handle_to_imap_socket() because we internally buffer the IMAP socket, so we can't just use it to copy straight from the socket handle β€” we have to copy the contents of our buffer first. 🫢
⚠️ The number of bytes specified must be available on the IMAP socket. If the function runs out of data it will literally 'die' with an error. Dramatic much? πŸ’€
  • _quote($String) β€” returns an IMAP quoted version of a string. It plops "..." around the string and replaces any internal " with \". Wrapping it up cute πŸŽ€

🧚 INTERNAL PARSING FUNCTIONS 🧚

  • _parse_list_to_hash($ListRef, $Recursive) β€” parses an array reference list of ($Key, $Value) pairs into a hash. Makes sure all the keys are lower cased (lc) first. lowercase supremacy πŸ’…
  • _fix_folder_name($FolderName, %Opts) β€” changes a folder name based on the current root folder prefix, as set with the set_root_prefix() call.
Wildcard => 1 = a folder name with % or * is left alone
NoEncoding => 1 = don't do modified utf-7 encoding, leave as unicode
  • _fix_folder_encoding($FolderName) β€” encodes folder name using IMAP-UTF-7.
  • _unfix_folder_name($FolderName) β€” un-changes a folder name based on the current root folder prefix as set with the set_root_prefix() call. Undoing the thing, reverse UNO πŸƒ
  • _parse_email_address($EmailAddressList) β€” converts a list of IMAP email address structures (as parsed and returned from an IMAP fetch (envelope) call) into a single RFC 822 email string (like "Person 1 Name" <ename@ecorp.com>, "Person 2 Name" <...>, etc) and finally returns it to you. This one's used to parse an envelope structure returned from a fetch call. Check the 'FETCH RESULTS' docs section for more. πŸ“¬
  • _parse_envelope($Envelope, $IncludeRaw, $DecodeUTF8) β€” converts an IMAP envelope structure (as parsed and returned from an IMAP fetch (envelope) call) into a convenient hash structure.

If $IncludeRaw is true, it includes the XXX-Raw fields β€” otherwise those get left out.

If $DecodeUTF8 is true, then it checks whether the fields contain any quoted-printable chars and decodes them to a Perl UTF8 string if they do.

See the 'FETCH RESULTS' docs section for more info. πŸ‘‘

  • _parse_bodystructure($BodyStructure, $IncludeRaw, $DecodeUTF8, $PartNum) β€” parses a standard IMAP body structure and turns it into a Perl friendly nested hash structure. This routine is recursive, so do not pass a value for $PartNum when you're calling it for the top level bodystructure item, okay? Also heads up: this routine destroys the array reference structure passed in as $BodyStructure. Gone. Destroyed. πŸ’€

See the 'FETCH RESULTS' docs section for more info.

  • _parse_fetch_annotation($AnnotateItem) β€” takes the result from a single IMAP annotation item and turns it into a Perl friendly structure. See the 'FETCH RESULTS' docs section for more info.
  • _parse_fetch_result($FetchResult) β€” takes the result from a single IMAP fetch response line and parses it into a Perl friendly structure. See the 'FETCH RESULTS' docs section for more info.
  • _parse_header_result($HeaderResults, $Value, $FetchResult) β€” takes a body[header.fields (xyz)] fetch response and parses out the header fields and values. 🌸
  • _decode_utf8($Value) β€” decodes the passed quoted printable value into a Perl Unicode string.
  • _expand_sequence(@Sequences) β€” expands a list of IMAP id sequences into a full list of ids. Blowing it up into the full roster ✨

πŸͺ PERL METHODS πŸͺ

  • DESTROY() β€” called by Perl when this object gets destroyed. Logs out of the IMAP server if it's still connected. The farewell arc 😭

πŸ” GMAIL, OFFICE 365, YAHOO and OAUTHBEARER/XOAUTH2 πŸ”

GMail, Office 365 and Yahoo are all moving to OAUTH authentication using the XOAUTH2 SASL mechanism. Authen::SASL 2.1800 and above support OAUTHBEARER and XOAUTH2 β€” so if you've got that version or greater, you can just use this:

my $sasl = Authen::SASL->new(
  mechanism => 'XOAUTH2' / 'OAUTHBEARER',
  callback => {
    user => $username,
    pass => $token,
  }
);

$login_res = $connection->authenticate($sasl);

If you're on an older version, no worries bestie, you can use this simple module instead:

package Authen::SASL::Perl::XOAUTH2;

use strict;
use warnings
use parent qw(Authen::SASL::Perl);

sub _order { 1 }
my @FLAGS;
sub _secflags { @FLAGS }
sub mechanism { 'XOAUTH2' }

sub client_start {
  my $self = shift;

  $self->{error} = undef;
  $self->{need_step} = 0;

  my ($user, $pass) = map {
    my $v = $self->_call($_);
    defined($v) ? $v : ''
  } qw(user pass);

  return "user=$user\001auth=Bearer $pass\001\001";
}

1;

And then authenticate using:

my $sasl = Authen::SASL->new(
  mechanism => 'XOAUTH2',
  callback => {
    user => $username,
    pass => $token,
  }
);

$login_res = $connection->authenticate($sasl);

πŸ‘€ SEE ALSO πŸ‘€

Net::IMAP, Mail::IMAPClient, IMAP::Admin, RFC 3501

Latest news/details can also be found at:

http://cpan.robm.fastmail.fm/mailimaptalk/

Available on github at:

<https://github.com/robmueller/mail-imaptalk/>


🌟 AUTHOR 🌟

Rob Mueller <cpan@robm.fastmail.fm>. Thanks to Jeremy Howard <j+daemonize@howard.fm> for socket code, support and documentation setup. Legends, both of them πŸ«ΆπŸ‘‘

Okay bestie, the legal tea β˜• β€” and honestly it's the good kind of legal tea, the kind where nobody's getting sued:

Copyright (C) 2003-2016 by FastMail Pty Ltd

That's it, that's the whole ownership sitch. FastMail held it down from 2003 all the way to 2016. πŸ‘‘


Now the part everyone actually cares about, aka can I use this in my project? and the answer is a resounding slay, yes:

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

Same terms as Perl itself. That's the whole license. No weird hoops, no "email us for permission," no fine print written by a lawyer at 3am. If it works for Perl, it works for this. 🫢

πŸ’€ Real talk though: it says you can redistribute and/or modify it β€” it does not say anything else about warranty, liability, or anything beyond those terms. So don't go reading extra promises into it that aren't there. The original means what it means. Periodt.

🌸 so anyway, that's the whole page 🌸

We made it. All nine parts. You now know Mail::IMAPTalk better than it knows itself, and honestly? Iconic behaviour. πŸ’…βœ¨

Little final vibe check: this page is from the perl v5.40.1 distribution of the docs, dated 2025-10-04, and it's the manual page for Mail::IMAPTalk(3pm) β€” so if you're ever lost in a terminal at 2am wondering which page you're reading, that's your answer, bestie. πŸŽ€πŸ¦‹

Go forth and speak IMAP fluently. I'm so proud of you. πŸ˜­πŸ’œ

πŸ“¦ where this page comes from