A Ruby wrapper around Net::IMAP with a hash-based search interface and messages which answer questions rather than handing back structs.
- Hash-based search criteria, including OR, NOT and repeated keys
- Message accessors for dates, flags, size, addresses and attachments
- RFC 2047 decoding, which Net::IMAP does not do
- Attachment filenames read out of the BODYSTRUCTURE, which IMAP cannot search upon
- One fetch for a whole set of messages rather than one per attribute
- BODY.PEEK throughout, so that reading a message does not mark it read
- No Rails dependency - works anywhere
- Zero external dependencies beyond net-imap
Add this line to your application's Gemfile:
gem 'imap'And then execute:
$ bundle installOr install it yourself as:
$ gem install imapThe gem is published under both imap and imap.rb. They are the same code; install one.
require 'imap'
imap_client = Imap.setup(
server: 'imap.example.com',
username: 'someone@example.com',
password: 'bigsecret',
mailbox: 'INBOX' # optional/default
)Imap.setup raises Imap::LoginFailed where the server rejects the password, or where only one of the username and the password is given. Imap#login answers true or false for a caller which would rather handle it itself.
Imap#mailboxes lists every mailbox which can be selected:
imap_client.mailboxes # => ['INBOX', 'INBOX.Archive', ...]
imap_client.mailbox = 'INBOX.Archive'Criteria are a hash, keyed by the IMAP search keys in lower case:
messages = imap_client.search(
from: 'noreply@example.com',
subject: 'Payday Loans',
since: '18-Dec-2025',
seen: false
)or: takes a list of criteria hashes. IMAP's OR takes two keys, so three or more nest for you:
imap_client.search(or: [{from: 'a@x'}, {to: 'b@y'}, {cc: 'c@z'}])not: negates each key it carries:
imap_client.search(not: {subject: 'Payday'})all_of: is how one key carries several terms, a hash holding it only once:
imap_client.search(all_of: [{text: 'invoice'}, {text: 'overdue'}])header: takes the field and the value:
imap_client.search(header: ['List-Id', 'ruby-talk'])An unrecognised key raises Imap::Search::UnknownSearchKey rather than widening the search silently.
Where the search keys are built elsewhere, Imap::Message.for does the fetch on its own:
Imap::Message.for(imap_client, imap_client.imap.search(['ALL']))A search fetches UID, FLAGS, INTERNALDATE, RFC822.SIZE, ENVELOPE and BODYSTRUCTURE for the whole set in one command, so none of these costs a round trip:
messages.each do |message|
message.uid
message.flags # => [:Seen]
message.seen?
message.size # bytes
message.received_at # Time, from INTERNALDATE
message.sent_at # Time, from the Date header
message.subject # encoded words decoded
message.from # => 'Café <billing@example.com>'
message.from_address # => 'billing@example.com'
message.from_domain # => 'example.com'
message.to # => ['user@example.com']
message.cc
message.attachment?
message.attachment_filenames # => ['résumé.pdf']
endThe body is fetched only when it is asked for:
message.body # BODY[TEXT], undecoded
message.source # the whole message, headers and all
message.urls # the URLs found in the body
message.mark_as_read#body is the raw text part. Choosing a part out of a multipart message and undoing its transfer encoding is MIME work rather than IMAP work, so hand #source to the mail gem where that is wanted:
mail = Mail.read_from_string(message.source)Imap::Message.decode reads RFC 2047 encoded words, which headers carry wherever a subject or a name is not ASCII:
Imap::Message.decode('=?UTF-8?B?UsOpc3Vtw6k=?=') # => 'Résumé'Imap::Message.search and .for build with new, so a subclass is handed back its own kind:
class Message < Imap::Message
def body
@body ||= Mail.read_from_string(source).text_part.decoded
end
end
Message.for(imap_client, message_ids).first.class # => Messageimap_client.bye- Fork it (https://github.com/thoran/imap/fork)
- Create your feature branch (
git checkout -b my-new-feature) - Commit your changes (
git commit -am 'Add some feature') - Push to the branch (
git push origin my-new-feature) - Create a new pull request
MIT