Postpeek
Postpeek is an ActionMailer delivery method for Rails development that intercepts outgoing emails, decodes their content, and stores them as structured directories on disk. Each email gets its own folder containing the raw source, decoded HTML and plain-text bodies, attachments, and a rich metadata JSON file — ready to be inspected by the companion postpeek-ui web interface or any other tool.
Installation
Add to your Gemfile:
gem "postpeek", group: :developmentThen run:
bundle installSetup
1. Configure the delivery method
In config/environments/development.rb:
config.action_mailer.delivery_method = :postpeek2. Create an initializer
config/initializers/postpeek.rb:
Postpeek.configure do |c|
# Where to store emails. Defaults to Rails.root/tmp/postpeek.
c.storage_path = Rails.root.join("tmp", "postpeek")
# Maximum number of emails to keep on disk. Oldest are pruned automatically.
# Set to nil to keep everything.
c.max_emails = 100
# Auto-prune old emails when max_emails is exceeded.
c.auto_prune = true
# How to handle emails with an unrecognised Content-Transfer-Encoding:
# :raise — raise Postpeek::Decoders::UnknownEncodingError (default)
# :warn — log a warning and skip the part
# :skip — silently skip the part
c.unknown_encoding = :raise
endStored Layout
Each intercepted email is saved under <storage_path>/<id>/:
tmp/postpeek/
└── 20240615T120000_a1b2c3d4/
├── metadata.json # structured metadata
├── message.eml # raw RFC 2822 source
├── rich.html # decoded HTML part (if present)
├── plain.txt # decoded plain-text part (if present)
└── attachments/
└── report.pdf # decoded attachment binaries
metadata.json schema
{
"id": "20240615T120000_a1b2c3d4",
"subject": "Welcome to Acme!",
"from": ["app@acme.com"],
"to": ["user@example.com"],
"cc": [],
"bcc": [],
"reply_to": null,
"sent_at": "2024-06-15T12:00:00Z",
"content_type": "multipart/alternative",
"parts": [
{ "content_type": "text/html", "encoding": "quoted-printable", "charset": "UTF-8" },
{ "content_type": "text/plain", "encoding": "7bit", "charset": "UTF-8" }
],
"encoding": null,
"locale": "en",
"attachments": [],
"message_id": "abc123@acme.com",
"mailer_class": "UserMailer",
"mailer_action": "welcome",
"delivery_duration_ms": 3,
"tags": []
}After-Save Hooks
Run custom code after each email is stored:
Postpeek.configure do |c|
c.after_save_hook do |message|
Rails.logger.info "Postpeek saved: #{message.id} — #{message.subject}"
end
endThe message object exposes:
| Method | Returns |
|---|---|
message.id |
String — storage ID |
message.subject |
String | nil |
message.from |
Array<String> |
message.to |
Array<String> |
message.mailer_class |
String | nil |
message.mailer_action |
String | nil |
message.html_body |
String | nil |
message.text_body |
String | nil |
message.attachments |
Array<Hash> |
message.metadata |
Hash — full metadata |
message.raw_source |
String — RFC 2822 source |
Errors raised inside a hook are rescued and printed to $stderr so one failing hook never blocks others.
Custom Metadata Builders
Extend the metadata hash with your own fields:
Postpeek.configure do |c|
c.metadata_builder do |metadata|
metadata.merge(
app_version: ENV["APP_VERSION"],
request_id: Thread.current[:current_request_id]
)
end
endEach callable receives the current metadata hash and must return a Hash. The returned hash is merged into the final metadata. Returning a non-Hash raises ArgumentError.
Known Limitations (v0.1)
-
deliver_lateris not supported. Active Job runs the delivery in a worker thread where the ActionMailer context (mailer_class,mailer_action) is no longer present. Both fields will benilin metadata. Usedeliver_nowin development. - The storage backend is filesystem-only in v0.1. Custom backends can be passed as a class:
c.storage_backend = MyBackend.
Development
bin/setup # install dependencies
bundle exec rspec # run the test suite
bundle exec rubocopContributing
Bug reports and pull requests are welcome on GitHub at https://github.com/mmarusyk/postpeek. This project follows the Contributor Covenant code of conduct.
License
The gem is available as open source under the MIT License.