|
| 1 | +# Design: Email Attachments Support (#154) |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +Follows the same pattern as the CC feature (#107). Three-level configuration, no database changes, integrates into existing mailer helpers. |
| 6 | + |
| 7 | +## Attachment Specification Format |
| 8 | + |
| 9 | +```ruby |
| 10 | +{ |
| 11 | + filename: String, # Required |
| 12 | + content: String/Binary, # Either :content or :path required |
| 13 | + path: String, # Either :content or :path required |
| 14 | + mime_type: String # Optional, inferred from filename if omitted |
| 15 | +} |
| 16 | +``` |
| 17 | + |
| 18 | +## Configuration Levels |
| 19 | + |
| 20 | +### 1. Global (`config.mailer_attachments`) |
| 21 | + |
| 22 | +```ruby |
| 23 | +# Single attachment |
| 24 | +config.mailer_attachments = { filename: 'terms.pdf', path: Rails.root.join('public', 'terms.pdf') } |
| 25 | + |
| 26 | +# Multiple attachments |
| 27 | +config.mailer_attachments = [ |
| 28 | + { filename: 'logo.png', path: Rails.root.join('app/assets/images/logo.png') }, |
| 29 | + { filename: 'terms.pdf', content: generate_pdf } |
| 30 | +] |
| 31 | + |
| 32 | +# Dynamic (Proc receives notification key) |
| 33 | +config.mailer_attachments = ->(key) { |
| 34 | + key.include?('invoice') ? { filename: 'invoice.pdf', content: generate_invoice } : nil |
| 35 | +} |
| 36 | +``` |
| 37 | + |
| 38 | +### 2. Target (`target.mailer_attachments`) |
| 39 | + |
| 40 | +```ruby |
| 41 | +class User < ActiveRecord::Base |
| 42 | + acts_as_target email: :email |
| 43 | + |
| 44 | + def mailer_attachments |
| 45 | + admin? ? { filename: 'admin_guide.pdf', path: '/path/to/guide.pdf' } : nil |
| 46 | + end |
| 47 | +end |
| 48 | +``` |
| 49 | + |
| 50 | +### 3. Notifiable Override (`notifiable.overriding_notification_email_attachments`) |
| 51 | + |
| 52 | +```ruby |
| 53 | +class Invoice < ActiveRecord::Base |
| 54 | + acts_as_notifiable :users, targets: -> { ... } |
| 55 | + |
| 56 | + def overriding_notification_email_attachments(target, key) |
| 57 | + { filename: "invoice_#{id}.pdf", content: generate_pdf } |
| 58 | + end |
| 59 | +end |
| 60 | +``` |
| 61 | + |
| 62 | +## Implementation |
| 63 | + |
| 64 | +### config.rb |
| 65 | + |
| 66 | +Add `mailer_attachments` attribute, initialize to `nil`. |
| 67 | + |
| 68 | +### mailers/helpers.rb |
| 69 | + |
| 70 | +#### New method: `mailer_attachments(target)` |
| 71 | + |
| 72 | +Same pattern as `mailer_cc(target)`: |
| 73 | + |
| 74 | +```ruby |
| 75 | +def mailer_attachments(target) |
| 76 | + if target.respond_to?(:mailer_attachments) |
| 77 | + target.mailer_attachments |
| 78 | + elsif ActivityNotification.config.mailer_attachments.present? |
| 79 | + if ActivityNotification.config.mailer_attachments.is_a?(Proc) |
| 80 | + key = @notification ? @notification.key : nil |
| 81 | + ActivityNotification.config.mailer_attachments.call(key) |
| 82 | + else |
| 83 | + ActivityNotification.config.mailer_attachments |
| 84 | + end |
| 85 | + else |
| 86 | + nil |
| 87 | + end |
| 88 | +end |
| 89 | +``` |
| 90 | + |
| 91 | +#### New method: `resolve_attachments(key)` |
| 92 | + |
| 93 | +Resolve with notifiable override priority: |
| 94 | + |
| 95 | +```ruby |
| 96 | +def resolve_attachments(key) |
| 97 | + if @notification&.notifiable&.respond_to?(:overriding_notification_email_attachments) && |
| 98 | + @notification.notifiable.overriding_notification_email_attachments(@target, key).present? |
| 99 | + @notification.notifiable.overriding_notification_email_attachments(@target, key) |
| 100 | + else |
| 101 | + mailer_attachments(@target) |
| 102 | + end |
| 103 | +end |
| 104 | +``` |
| 105 | + |
| 106 | +#### New method: `process_attachments(mail_obj, specs)` |
| 107 | + |
| 108 | +```ruby |
| 109 | +def process_attachments(mail_obj, specs) |
| 110 | + return if specs.blank? |
| 111 | + Array(specs).each do |spec| |
| 112 | + next if spec.blank? |
| 113 | + validate_attachment_spec!(spec) |
| 114 | + content = spec[:content] || File.read(spec[:path]) |
| 115 | + options = { content: content } |
| 116 | + options[:mime_type] = spec[:mime_type] if spec[:mime_type] |
| 117 | + mail_obj.attachments[spec[:filename]] = options |
| 118 | + end |
| 119 | +end |
| 120 | +``` |
| 121 | + |
| 122 | +#### Modified: `headers_for` |
| 123 | + |
| 124 | +Add attachment resolution, store in headers: |
| 125 | + |
| 126 | +```ruby |
| 127 | +attachment_specs = resolve_attachments(key) |
| 128 | +headers[:attachment_specs] = attachment_specs if attachment_specs.present? |
| 129 | +``` |
| 130 | + |
| 131 | +#### Modified: `send_mail` |
| 132 | + |
| 133 | +Extract and process attachments: |
| 134 | + |
| 135 | +```ruby |
| 136 | +def send_mail(headers, fallback = nil) |
| 137 | + attachment_specs = headers.delete(:attachment_specs) |
| 138 | + begin |
| 139 | + mail_obj = mail headers |
| 140 | + process_attachments(mail_obj, attachment_specs) |
| 141 | + mail_obj |
| 142 | + rescue ActionView::MissingTemplate => e |
| 143 | + if fallback.present? |
| 144 | + mail_obj = mail headers.merge(template_name: fallback) |
| 145 | + process_attachments(mail_obj, attachment_specs) |
| 146 | + mail_obj |
| 147 | + else |
| 148 | + raise e |
| 149 | + end |
| 150 | + end |
| 151 | +end |
| 152 | +``` |
| 153 | + |
| 154 | +### Generator Template |
| 155 | + |
| 156 | +Add commented configuration example to `activity_notification.rb` template. |
0 commit comments