File: action_mailbox_basics.md

package info (click to toggle)
rails 2%3A7.2.2.1%2Bdfsg-7
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 43,352 kB
  • sloc: ruby: 349,799; javascript: 30,703; yacc: 46; sql: 43; sh: 29; makefile: 27
file content (526 lines) | stat: -rw-r--r-- 18,818 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
**DO NOT READ THIS FILE ON GITHUB, GUIDES ARE PUBLISHED ON https://guides.rubyonrails.org.**

Action Mailbox Basics
=====================

This guide provides you with all you need to get started in receiving emails to
your application.

After reading this guide, you will know:

* How to receive email within a Rails application.
* How to configure Action Mailbox.
* How to generate and route emails to a mailbox.
* How to test incoming emails.

--------------------------------------------------------------------------------

What is Action Mailbox?
-----------------------

Action Mailbox routes incoming emails to controller-like mailboxes for
processing in your Rails application. Action Mailbox is for receiving email,
while [Action Mailer](action_mailer_basics.html) is for *sending* them.

The inbound emails are routed asynchronously using [Active
Job](active_job_basics.html) to one or several dedicated mailboxes. These emails
are turned into
[`InboundEmail`](https://api.rubyonrails.org/classes/ActionMailbox/InboundEmail.html)
records using [Active Record](active_record_basics.html), which are capable of
interacting directly with the rest of your domain model.

`InboundEmail` records also provide lifecycle tracking, storage of the original
email via [Active Storage](active_storage_overview.html), and responsible data
handling with on-by-default [incineration](#incineration-of-inboundemails).

Action Mailbox ships with ingresses which enable your application to receive
emails from external email providers such as Mailgun, Mandrill, Postmark, and
SendGrid. You can also handle inbound emails directly via the built-in Exim,
Postfix, and Qmail ingresses.

## Setup

Action Mailbox has a few moving parts. First, you'll run the installer. Next,
you'll choose and configure an ingress for handling incoming email. You're then
ready to add Action Mailbox routing, create mailboxes, and start processing
incoming emails.

To start, let's install Action Mailbox:

```bash
$ bin/rails action_mailbox:install
```

This will create an `application_mailbox.rb` file and copy over migrations.

```bash
$ bin/rails db:migrate
```

This will run the Action Mailbox and Active Storage migrations.

The Action Mailbox table `action_mailbox_inbound_emails` stores incoming
messages and their processing status.

At this point, you can start your Rails server and check out
`http://localhost:3000/rails/conductor/action_mailbox/inbound_emails`. See
[Local Development and Testing](#local-development-and-testing) for more.

The next step is to configure an ingress in your Rails application to specify
how incoming emails should be received.

## Ingress Configuration

Configuring ingress involves setting up credentials and endpoint information for
the chosen email service. Here are the steps for each of the supported
ingresses.

### Exim

Tell Action Mailbox to accept emails from an SMTP relay:

```ruby
# config/environments/production.rb
config.action_mailbox.ingress = :relay
```

Generate a strong password that Action Mailbox can use to authenticate requests
to the relay ingress.

Use `bin/rails credentials:edit` to add the password to your application's
encrypted credentials under `action_mailbox.ingress_password`, where Action
Mailbox will automatically find it:

```yaml
action_mailbox:
  ingress_password: ...
```

Alternatively, provide the password in the `RAILS_INBOUND_EMAIL_PASSWORD`
environment variable.

Configure Exim to pipe inbound emails to `bin/rails
action_mailbox:ingress:exim`, providing the `URL` of the relay ingress and the
`INGRESS_PASSWORD` you previously generated. If your application lived at
`https://example.com`, the full command would look like this:

```bash
$ bin/rails action_mailbox:ingress:exim URL=https://example.com/rails/action_mailbox/relay/inbound_emails INGRESS_PASSWORD=...
```

### Mailgun

Give Action Mailbox your Mailgun Signing key (which you can find under Settings
-> Security & Users -> API security in Mailgun), so it can authenticate requests
to the Mailgun ingress.

Use `bin/rails credentials:edit` to add your Signing key to your application's
encrypted credentials under `action_mailbox.mailgun_signing_key`, where Action
Mailbox will automatically find it:

```yaml
action_mailbox:
  mailgun_signing_key: ...
```

Alternatively, provide your Signing key in the `MAILGUN_INGRESS_SIGNING_KEY`
environment variable.

Tell Action Mailbox to accept emails from Mailgun:

```ruby
# config/environments/production.rb
config.action_mailbox.ingress = :mailgun
```

[Configure
Mailgun](https://documentation.mailgun.com/docs/mailgun/user-manual/receive-forward-store/)
to forward inbound emails to
`/rails/action_mailbox/mailgun/inbound_emails/mime`. If your application lived
at `https://example.com`, you would specify the fully-qualified URL
`https://example.com/rails/action_mailbox/mailgun/inbound_emails/mime`.

### Mandrill

Give Action Mailbox your Mandrill API key, so it can authenticate requests to
the Mandrill ingress.

Use `bin/rails credentials:edit` to add your API key to your application's
encrypted credentials under `action_mailbox.mandrill_api_key`, where Action
Mailbox will automatically find it:

```yaml
action_mailbox:
  mandrill_api_key: ...
```

Alternatively, provide your API key in the `MANDRILL_INGRESS_API_KEY`
environment variable.

Tell Action Mailbox to accept emails from Mandrill:

```ruby
# config/environments/production.rb
config.action_mailbox.ingress = :mandrill
```

[Configure
Mandrill](https://mandrill.zendesk.com/hc/en-us/articles/205583197-Inbound-Email-Processing-Overview)
to route inbound emails to `/rails/action_mailbox/mandrill/inbound_emails`. If
your application lived at `https://example.com`, you would specify the
fully-qualified URL
`https://example.com/rails/action_mailbox/mandrill/inbound_emails`.

### Postfix

Tell Action Mailbox to accept emails from an SMTP relay:

```ruby
# config/environments/production.rb
config.action_mailbox.ingress = :relay
```

Generate a strong password that Action Mailbox can use to authenticate requests
to the relay ingress.

Use `bin/rails credentials:edit` to add the password to your application's
encrypted credentials under `action_mailbox.ingress_password`, where Action
Mailbox will automatically find it:

```yaml
action_mailbox:
  ingress_password: ...
```

Alternatively, provide the password in the `RAILS_INBOUND_EMAIL_PASSWORD`
environment variable.

[Configure
Postfix](https://serverfault.com/questions/258469/how-to-configure-postfix-to-pipe-all-incoming-email-to-a-script)
to pipe inbound emails to `bin/rails action_mailbox:ingress:postfix`, providing
the `URL` of the Postfix ingress and the `INGRESS_PASSWORD` you previously
generated. If your application lived at `https://example.com`, the full command
would look like this:

```bash
$ bin/rails action_mailbox:ingress:postfix URL=https://example.com/rails/action_mailbox/relay/inbound_emails INGRESS_PASSWORD=...
```

### Postmark

Tell Action Mailbox to accept emails from Postmark:

```ruby
# config/environments/production.rb
config.action_mailbox.ingress = :postmark
```

Generate a strong password that Action Mailbox can use to authenticate requests
to the Postmark ingress.

Use `bin/rails credentials:edit` to add the password to your application's
encrypted credentials under `action_mailbox.ingress_password`, where Action
Mailbox will automatically find it:

```yaml
action_mailbox:
  ingress_password: ...
```

Alternatively, provide the password in the `RAILS_INBOUND_EMAIL_PASSWORD`
environment variable.

[Configure Postmark inbound
webhook](https://postmarkapp.com/manual#configure-your-inbound-webhook-url) to
forward inbound emails to `/rails/action_mailbox/postmark/inbound_emails` with
the username `actionmailbox` and the password you previously generated. If your
application lived at `https://example.com`, you would configure Postmark with
the following fully-qualified URL:

```
https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/postmark/inbound_emails
```

NOTE: When configuring your Postmark inbound webhook, be sure to check the box
labeled **"Include raw email content in JSON payload"**. Action Mailbox needs
the raw email content to work.

### Qmail

Tell Action Mailbox to accept emails from an SMTP relay:

```ruby
# config/environments/production.rb
config.action_mailbox.ingress = :relay
```

Generate a strong password that Action Mailbox can use to authenticate requests
to the relay ingress.

Use `bin/rails credentials:edit` to add the password to your application's
encrypted credentials under `action_mailbox.ingress_password`, where Action
Mailbox will automatically find it:

```yaml
action_mailbox:
  ingress_password: ...
```

Alternatively, provide the password in the `RAILS_INBOUND_EMAIL_PASSWORD`
environment variable.

Configure Qmail to pipe inbound emails to `bin/rails
action_mailbox:ingress:qmail`, providing the `URL` of the relay ingress and the
`INGRESS_PASSWORD` you previously generated. If your application lived at
`https://example.com`, the full command would look like this:

```bash
$ bin/rails action_mailbox:ingress:qmail URL=https://example.com/rails/action_mailbox/relay/inbound_emails INGRESS_PASSWORD=...
```

### SendGrid

Tell Action Mailbox to accept emails from SendGrid:

```ruby
# config/environments/production.rb
config.action_mailbox.ingress = :sendgrid
```

Generate a strong password that Action Mailbox can use to authenticate requests
to the SendGrid ingress.

Use `bin/rails credentials:edit` to add the password to your application's
encrypted credentials under `action_mailbox.ingress_password`, where Action
Mailbox will automatically find it:

```yaml
action_mailbox:
  ingress_password: ...
```

Alternatively, provide the password in the `RAILS_INBOUND_EMAIL_PASSWORD`
environment variable.

[Configure SendGrid Inbound
Parse](https://sendgrid.com/docs/for-developers/parsing-email/setting-up-the-inbound-parse-webhook/)
to forward inbound emails to `/rails/action_mailbox/sendgrid/inbound_emails`
with the username `actionmailbox` and the password you previously generated. If
your application lived at `https://example.com`, you would configure SendGrid
with the following URL:

```
https://actionmailbox:PASSWORD@example.com/rails/action_mailbox/sendgrid/inbound_emails
```

NOTE: When configuring your SendGrid Inbound Parse webhook, be sure to check the
box labeled **“Post the raw, full MIME message.”** Action Mailbox needs the raw
MIME message to work.

## Processing Incoming Email

Processing incoming emails usually entails using the email content to create
models, update views, queue background work, etc. in your Rails application.

Before you can start processing incoming emails, you'll need to setup Action
Mailbox routing and create mailboxes.

### Configure Routing

After an incoming email is received via the configured ingress, it needs to be
forwarded to a mailbox for actual processing by your application. Much like the
[Rails router](routing.html) that dispatches URLs to controllers, routing in
Action Mailbox defines which emails go to which mailboxes for processing. Routes
are added to the `application_mailbox.rb` file using regular expressions:

```ruby
# app/mailboxes/application_mailbox.rb
class ApplicationMailbox < ActionMailbox::Base
  routing(/^save@/i     => :forwards)
  routing(/@replies\./i => :replies)
end
```

The regular expression matches the incoming email's `to`, `cc`, or `bcc` fields.
For example, the above will match any email sent to `save@` to a "forwards"
mailbox. There are other ways to route an email, see
[`ActionMailbox::Base`](https://api.rubyonrails.org/classes/ActionMailbox/Base.html)
for more.

We need to create that "forwards" mailbox next.

### Create a Mailbox

```bash
# Generate new mailbox
$ bin/rails generate mailbox forwards
```

This creates `app/mailboxes/forwards_mailbox.rb`, with a `ForwardsMailbox` class
and a `process` method.

### Process Email

When processing an `InboundEmail`, you can get the parsed version of the email
as a [`Mail`](https://github.com/mikel/mail) object with `InboundEmail#mail`.
You can also get the raw source directly using the `#source` method. With the
`Mail` object, you can access the relevant fields, such as `mail.to`,
`mail.body.decoded`, etc.

```irb
irb> mail
=> #<Mail::Message:33780, Multipart: false, Headers: <Date: Wed, 31 Jan 2024 22:18:40 -0600>, <From: someone@hey.com>, <To: save@example.com>, <Message-ID: <65bb1ba066830_50303a70397e@Bhumis-MacBook-Pro.local.mail>>, <In-Reply-To: >, <Subject: Hello Action Mailbox>, <Mime-Version: 1.0>, <Content-Type: text/plain; charset=UTF-8>, <Content-Transfer-Encoding: 7bit>, <x-original-to: >>
irb> mail.to
=> ["save@example.com"]
irb> mail.from
=> ["someone@hey.com"]
irb> mail.date
=> Wed, 31 Jan 2024 22:18:40 -0600
irb> mail.subject
=> "Hello Action Mailbox"
irb> mail.body.decoded
=> "This is the body of the email message."
# mail.decoded, a shorthand for mail.body.decoded, also works
irb> mail.decoded
=> "This is the body of the email message."
irb> mail.body
=> <Mail::Body:0x00007fc74cbf46c0 @boundary=nil, @preamble=nil, @epilogue=nil, @charset="US-ASCII", @part_sort_order=["text/plain", "text/enriched", "text/html", "multipart/alternative"], @parts=[], @raw_source="This is the body of the email message.", @ascii_only=true, @encoding="7bit">
```

### Inbound Email Status

While the email is being routed to a matching mailbox and processed, Action
Mailbox updates the email status stored in `action_mailbox_inbound_emails` table
with one of the following values:

- `pending`: Received by one of the ingress controllers and scheduled for
  routing.
- `processing`: During active processing, while a specific mailbox is running
  its `process` method.
- `delivered`: Successfully processed by the specific mailbox.
- `failed`: An exception was raised during the specific mailbox’s execution of
  the `process` method.
- `bounced`: Rejected processing by the specific mailbox and bounced to sender.

If the email is marked either `delivered`, `failed`, or `bounced` it's
considered "processed" and marked for
[incineration](#incineration-of-inboundemails).

## Example

Here is an example of an Action Mailbox that processes emails to create
"forwards" for the user's project.

The `before_processing` callback is used to ensure that certain conditions are
met before `process` method is called. In this case, `before_processing` checks
that the user has at least one project. Other supported [Action Mailbox
callbacks](https://api.rubyonrails.org/classes/ActionMailbox/Callbacks.html) are
`after_processing` and `around_processing`.

The email can be bounced using `bounced_with` if the "forwarder" has no
projects. The "forwarder" is a `User` with the same email as `mail.from`.

If the "forwarder" does have at least one project, the `record_forward` method
creates an Active Record model in the application using the email data
`mail.subject` and `mail.decoded`. Otherwise, it sends an email, using Action
Mailer, requesting the "forwarder" to choose a project.

```ruby
# app/mailboxes/forwards_mailbox.rb
class ForwardsMailbox < ApplicationMailbox
  # Callbacks specify prerequisites to processing
  before_processing :require_projects

  def process
    # Record the forward on the one project, or…
    if forwarder.projects.one?
      record_forward
    else
      # …involve a second Action Mailer to ask which project to forward into.
      request_forwarding_project
    end
  end

  private
    def require_projects
      if forwarder.projects.none?
        # Use Action Mailers to bounce incoming emails back to sender – this halts processing
        bounce_with Forwards::BounceMailer.no_projects(inbound_email, forwarder: forwarder)
      end
    end

    def record_forward
      forwarder.forwards.create subject: mail.subject, content: mail.decoded
    end

    def request_forwarding_project
      Forwards::RoutingMailer.choose_project(inbound_email, forwarder: forwarder).deliver_now
    end

    def forwarder
      @forwarder ||= User.find_by(email_address: mail.from)
    end
end
```

## Local Development and Testing

It's helpful to be able to test incoming emails in development without actually
sending and receiving real emails. To accomplish this, there's a conductor
controller mounted at `/rails/conductor/action_mailbox/inbound_emails`, which
gives you an index of all the InboundEmails in the system, their state of
processing, and a form to create a new InboundEmail as well.

Here is and example of testing an inbound email with Action Mailbox TestHelpers.

```ruby
class ForwardsMailboxTest < ActionMailbox::TestCase
  test "directly recording a client forward for a forwarder and forwardee corresponding to one project" do
    assert_difference -> { people(:david).buckets.first.recordings.count } do
      receive_inbound_email_from_mail \
        to: 'save@example.com',
        from: people(:david).email_address,
        subject: "Fwd: Status update?",
        body: <<~BODY
          --- Begin forwarded message ---
          From: Frank Holland <frank@microsoft.com>

          What's the status?
        BODY
    end

    recording = people(:david).buckets.first.recordings.last
    assert_equal people(:david), recording.creator
    assert_equal "Status update?", recording.forward.subject
    assert_match "What's the status?", recording.forward.content.to_s
  end
end
```

Please refer to the [ActionMailbox::TestHelper
API](https://api.rubyonrails.org/classes/ActionMailbox/TestHelper.html) for
further test helper methods.

## Incineration of InboundEmails

By default, an `InboundEmail` that has been processed will be incinerated after
30 days. The `InboundEmail` is considered as processed when its status changes
to `delivered`, `failed`, or `bounced`.

The actual incineration is done via the
[`IncinerationJob`](https://api.rubyonrails.org/classes/ActionMailbox/IncinerationJob.html)
that's scheduled to run after
[`config.action_mailbox.incinerate_after`](configuring.html#config-action-mailbox-incinerate-after)
time. This value is set to `30.days` by default, but you can change it in your
production.rb configuration. (Note that this far-future incineration scheduling
relies on your job queue being able to hold jobs for that long.)

Default data incineration ensures that you're not holding on to people's data
unnecessarily after they may have canceled their accounts or deleted their
content.

The intention with Action Mailbox processing is that as you process an email,
you should extract all the data you need from the email and persist it into
domain models in your application. The `InboundEmail` stays in the system for
the configured time to allow for debugging and forensics and then will be
deleted.