courrier 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b2da5fe5fc7bb657449ed12d90c8443d6fa692339f9974fcbda7c4c726964ad8
4
- data.tar.gz: ad4fc570a0bdb7cb1bce3c214a21e550a725212e0f900e5d31fe1345d5c7b221
3
+ metadata.gz: 1359782140920863fe902186544d8b34bcbc513ca4412b26bc3bea975196916e
4
+ data.tar.gz: 493af39e04f09434689cb810a57c591b02ec9373b84660de4de0349c168eebfb
5
5
  SHA512:
6
- metadata.gz: 7170f0e34c59ceee7b88c41865da67f558357058651a9508578adb5b0fec0d5da78bea1672aed8b24335128822784c1e5b23478804fb1a08273efece5129ae3f
7
- data.tar.gz: 89de9523761715879b487d62ca63576eb9783647a0e2d8baba5ef268b36c91d359d96c33386b06a3cd686af8e747cf548eaf74ea9654efde22030a52eaac5fe4
6
+ metadata.gz: d7928a0eb201e6b4b5e36612a03641c49a862477c6338be08800e10c1a4b9cc5f8e1917253a2a2b9b414c51ef361c9dcbfe87a309e1fcba22fb8af43310f068e
7
+ data.tar.gz: 9a4c22a760d6c9af901beaa076a30dcbe02db45b2c339932bef2a3e6df90531d3bdaf860c86bb954d0397243b73c4b2b07dd4072eef6cd5c9d641a9e2f3303fd
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- courrier (0.6.0)
4
+ courrier (0.7.0)
5
5
  launchy (>= 3.1, < 4)
6
6
  nokogiri (>= 1.18, < 2)
7
7
 
data/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # Courrier
2
2
 
3
- Modern, API-powered email delivery for Ruby apps.
3
+ API-powered email delivery for Ruby apps.
4
4
 
5
- ![A cute cartoon mascot wearing a blue postal uniform with red scarf and cap, carrying a leather messenger bag, representing a modern email delivery system for Ruby applications](https://raw.githubusercontent.com/Rails-Designer/courrier/HEAD/.github/cover.jpg)
5
+ ![A cute cartoon mascot wearing a blue postal uniform with red scarf and cap, carrying a leather messenger bag, representing an API-powered email delivery system for Ruby applications](https://raw.githubusercontent.com/Rails-Designer/courrier/HEAD/.github/cover.jpg)
6
6
 
7
7
  ```ruby
8
8
  # Quick example
@@ -108,7 +108,8 @@ Provider and API key settings can be overridden using environment variables (`CO
108
108
 
109
109
  Besides the standard email attributes (`from`, `to`, `reply_to`, etc.), you can pass any additional attributes that will be available in your email templates:
110
110
  ```ruby
111
- OrderEmail.deliver to: "recipient@railsdesigner.com", download_url: downloads_path(token: "token")
111
+ OrderEmail.deliver to: "recipient@railsdesigner.com",\
112
+ download_url: downloads_path(token: "token")
112
113
  ```
113
114
 
114
115
  These custom attributes are accessible directly in your email class:
@@ -159,6 +160,7 @@ Courrier supports these transactional email providers:
159
160
  - [Mailjet](https://mailjet.com)
160
161
  - [MailPace](https://mailpace.com)
161
162
  - [Postmark](https://postmarkapp.com)
163
+ - [Resend](https://resend.com)
162
164
  - [SendGrid](https://sendgrid.com)
163
165
  - [SparkPost](https://sparkpost.com)
164
166
  - [Userlist](https://userlist.com)
@@ -168,17 +170,26 @@ Courrier supports these transactional email providers:
168
170
 
169
171
  ## More Features
170
172
 
171
- Additional functionality to help with development and email handling:
173
+ Additional functionality to help with development and testing:
174
+
172
175
 
176
+ ### Inbox (Rails only)
173
177
 
174
- ### Email Preview
178
+ You can preview your emails in the inbox:
179
+ ```ruby
180
+ config.provider = "inbox"
181
+
182
+ # And add to your routes:
183
+ mount Courrier::Engine => "/courrier"
184
+ ```
175
185
 
176
- Preview emails in your browser during development:
186
+ If you want to automatically open every email in your default browser:
177
187
  ```ruby
178
- config.provider = "preview" # Opens emails in your default browser
188
+ config.provider = "inbox"
189
+ config.inbox.auto_open = true
179
190
  ```
180
191
 
181
- Previews are automatically cleared with `bin/rails tmp:clear`, or manually with `bin/rails courrier:clear`.
192
+ Emails are automatically cleared with `bin/rails tmp:clear`, or manually with `bin/rails courrier:clear`.
182
193
 
183
194
 
184
195
  ### Layout Support
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Courrier
4
+ module Previews
5
+ class CleanupsController < ActionController::Base
6
+ def create
7
+ system("bin/rails courrier:clear")
8
+
9
+ redirect_to root_path
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Courrier
4
+ class PreviewsController < ActionController::Base
5
+ def index
6
+ @emails = emails.map { Courrier::Email::Providers::Inbox::Email.from_file(_1) }
7
+ end
8
+
9
+ def show
10
+ file_path = File.join(Courrier.configuration.inbox.destination, params[:id])
11
+ content = File.read(file_path)
12
+
13
+ render html: content.html_safe, layout: false
14
+ end
15
+
16
+ private
17
+
18
+ def emails
19
+ @emails ||= Dir.glob("#{Courrier.configuration.inbox.destination}/*.html")
20
+ .sort_by { -File.basename(_1, ".html").to_i }
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,171 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Courrier Inbox</title>
6
+
7
+ <style>
8
+ *, *::before, *::after { box-sizing: border-box; }
9
+ * { margin: 0; padding: 0; }
10
+ body { margin: 0; padding: 0; font-size: 16px; line-height: 1.5; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; background-color: rgb(241, 245, 249); }
11
+
12
+ .sidebar {
13
+ display: flex;
14
+ flex-direction: column;
15
+ position: sticky;
16
+ top: 0;
17
+ padding: .5rem;
18
+ width: min(350px, 33.3333%); height: 100dvh;
19
+ border-right: 1px solid rgb(226, 232, 240);
20
+ }
21
+
22
+ .sidebar__options {
23
+ position: sticky;
24
+ bottom: 0;
25
+ padding: .5rem;
26
+ background-color: rgb(241, 245, 249);
27
+ }
28
+
29
+ .sidebar__btn-clear {
30
+ border: none;
31
+ display: flex;
32
+ align-items: center;
33
+ justify-content: center;
34
+ column-gap: 0.25rem;
35
+ width: 100%;
36
+ padding: .5rem;
37
+ font-size: .875rem; line-height: 1.25rem;
38
+ font-weight: 500;
39
+ color: rgb(71, 85, 105);
40
+ background-color: rgb(226, 232, 240);
41
+ border-radius: .5rem;
42
+ cursor: default;
43
+
44
+ &:hover { background-color: rgb(203, 213, 225); }
45
+ &:active { transform: scale(.98); }
46
+
47
+ [data-slot="icon"] {
48
+ width: .875rem; height: .875rem;
49
+ }
50
+ }
51
+
52
+ .email-previews {
53
+ display: flex;
54
+ flex-direction: column;
55
+ flex: 1;
56
+ row-gap: .5rem;
57
+ overflow-y: auto;
58
+ width: 100%;
59
+ }
60
+
61
+ .email-preview {
62
+ display: flex;
63
+ flex-direction: row;
64
+ column-gap: .5rem;
65
+ width: 100%;
66
+ padding: .5rem .75rem;
67
+ text-decoration: none;
68
+ border-radius: .5rem;
69
+
70
+ &:hover {
71
+ background-color: rgba(226, 232, 240, .75);
72
+ }
73
+ }
74
+
75
+ .email-preview--empty {
76
+ display: none;
77
+ text-align: center;
78
+
79
+ &:only-child {
80
+ display: block;
81
+ }
82
+ }
83
+
84
+ .email-preview__avatar {
85
+ flex-shrink: 0;
86
+ padding: .25rem;
87
+ width: 1.5rem; height: 1.5rem;
88
+ background-color: rgb(203, 213, 225);
89
+ color: rgb(51, 65, 85);
90
+ border-radius: 9999px;
91
+ }
92
+
93
+ .email-preview__content {
94
+ font-size: 0.875rem; line-height: 1.25rem;
95
+ }
96
+
97
+ .email-preview__recipient {
98
+ font-size: 1rem; line-height: 1.5rem;
99
+ font-weight: 600;
100
+ letter-spacing: -.025em;
101
+ color: rgb(51, 65, 85);
102
+ }
103
+
104
+ .email-preview__subject {
105
+ margin-top: .125rem;
106
+ overflow: hidden; display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 1;
107
+ color: rgb(100, 116, 139);
108
+ }
109
+ </style>
110
+ </head>
111
+ <body>
112
+ <div style="display: flex; column-gap: 1rem;">
113
+ <aside class="sidebar">
114
+ <ul class="email-previews">
115
+ <li class="email-preview--empty">
116
+ <p class="email-preview__subject">
117
+ No emails yet…
118
+ </p>
119
+ </li>
120
+
121
+ <% @emails.each do |email| %>
122
+ <li>
123
+ <%= link_to courrier.preview_path(email.filename), data: {remote: true}, class: "email-preview" do %>
124
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" data-slot="icon" class="email-preview__avatar">
125
+ <path fill-rule="evenodd" d="M7.5 6a4.5 4.5 0 1 1 9 0 4.5 4.5 0 0 1-9 0ZM3.751 20.105a8.25 8.25 0 0 1 16.498 0 .75.75 0 0 1-.437.695A18.683 18.683 0 0 1 12 22.5c-2.786 0-5.433-.608-7.812-1.7a.75.75 0 0 1-.437-.695Z" clip-rule="evenodd"/>
126
+ </svg>
127
+
128
+ <div class="email-preview__content">
129
+ <small class="email-preview__recipient">
130
+ <%= email.metadata.to %>
131
+ </small>
132
+
133
+ <p class="email-preview__subject">
134
+ <%= email.metadata.subject || "No subject" %>
135
+ </p>
136
+ </div>
137
+ <% end %>
138
+ </li>
139
+ <% end %>
140
+ </ul>
141
+
142
+ <div class="sidebar__options">
143
+ <%= button_to courrier.cleanup_path, class: "sidebar__btn-clear" do %>
144
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" data-slot="icon">
145
+ <path fill-rule="evenodd" d="M16.5 4.478v.227a48.816 48.816 0 0 1 3.878.512.75.75 0 1 1-.256 1.478l-.209-.035-1.005 13.07a3 3 0 0 1-2.991 2.77H8.084a3 3 0 0 1-2.991-2.77L4.087 6.66l-.209.035a.75.75 0 0 1-.256-1.478A48.567 48.567 0 0 1 7.5 4.705v-.227c0-1.564 1.213-2.9 2.816-2.951a52.662 52.662 0 0 1 3.369 0c1.603.051 2.815 1.387 2.815 2.951Zm-6.136-1.452a51.196 51.196 0 0 1 3.273 0C14.39 3.05 15 3.684 15 4.478v.113a49.488 49.488 0 0 0-6 0v-.113c0-.794.609-1.428 1.364-1.452Zm-.355 5.945a.75.75 0 1 0-1.5.058l.347 9a.75.75 0 1 0 1.499-.058l-.346-9Zm5.48.058a.75.75 0 1 0-1.498-.058l-.347 9a.75.75 0 0 0 1.5.058l.345-9Z" clip-rule="evenodd"/>
146
+ </svg>
147
+
148
+ Clear inbox
149
+ <% end %>
150
+ </div>
151
+ </aside>
152
+
153
+ <article style="flex: 1;" id="email-preview">
154
+ </article>
155
+ </div>
156
+
157
+ <script>
158
+ document.querySelectorAll("a[data-remote]").forEach(link => {
159
+ link.addEventListener("click", (event) => {
160
+ event.preventDefault();
161
+
162
+ fetch(link.href)
163
+ .then(response => response.text())
164
+ .then(html => {
165
+ document.getElementById("email-preview").innerHTML = html;
166
+ });
167
+ });
168
+ });
169
+ </script>
170
+ </body>
171
+ </html>
data/config/routes.rb ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ Courrier::Engine.routes.draw do
4
+ resources :previews, only: %w[show], constraints: {id: /.*\.html/}
5
+ resource :cleanup, only: %w[create], module: "previews"
6
+
7
+ root "previews#index"
8
+ end
data/courrier.gemspec CHANGED
@@ -8,15 +8,15 @@ Gem::Specification.new do |spec|
8
8
  spec.authors = ["Rails Designer"]
9
9
  spec.email = ["devs@railsdesigner.com"]
10
10
 
11
- spec.summary = "Modern, API-powered email delivery for Rails apps"
12
- spec.description = "Modern, API-powered email delivery for Ruby apps with support for Postmark, SendGrid, Mailgun and more."
11
+ spec.summary = "API-powered email delivery for Ruby apps"
12
+ spec.description = "API-powered email delivery for Ruby apps with support for Postmark, SendGrid, Mailgun and more."
13
13
  spec.homepage = "https://railsdesigner.com/courrier/"
14
14
  spec.license = "MIT"
15
15
 
16
16
  spec.metadata["homepage_uri"] = spec.homepage
17
17
  spec.metadata["source_code_uri"] = "https://github.com/Rails-Designer/courrier/"
18
18
 
19
- spec.files = Dir["{bin,app,config,db,lib,public}/**/*", "Rakefile", "README.md", "courrier.gemspec", "Gemfile", "Gemfile.lock"]
19
+ spec.files = Dir["{bin,app,config,lib}/**/*", "Rakefile", "README.md", "courrier.gemspec", "Gemfile", "Gemfile.lock"]
20
20
 
21
21
  spec.required_ruby_version = ">= 3.2.0"
22
22
 
@@ -2,13 +2,13 @@
2
2
 
3
3
  module Courrier
4
4
  class Configuration
5
- class Preview
5
+ class Inbox
6
6
  attr_accessor :destination, :auto_open, :template_path
7
7
 
8
8
  def initialize
9
9
  @destination = default_destination
10
- @auto_open = true
11
- @template_path = File.expand_path("../../../courrier/email/providers/preview/default.html.erb", __FILE__)
10
+ @auto_open = false
11
+ @template_path = File.expand_path("../../../courrier/email/providers/inbox/default.html.erb", __FILE__)
12
12
  end
13
13
 
14
14
  private
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "courrier/configuration/preview"
3
+ require "courrier/configuration/inbox"
4
4
  require "courrier/configuration/providers"
5
5
 
6
6
  module Courrier
@@ -21,7 +21,7 @@ module Courrier
21
21
  class Configuration
22
22
  attr_accessor :provider, :api_key, :logger, :email_path, :layouts, :default_url_options, :auto_generate_text,
23
23
  :from, :reply_to, :cc, :bcc
24
- attr_reader :providers, :preview
24
+ attr_reader :providers, :inbox
25
25
 
26
26
  def initialize
27
27
  @provider = "logger"
@@ -39,7 +39,7 @@ module Courrier
39
39
  @bcc = nil
40
40
 
41
41
  @providers = Courrier::Configuration::Providers.new
42
- @preview = Courrier::Configuration::Preview.new
42
+ @inbox = Courrier::Configuration::Inbox.new
43
43
  end
44
44
 
45
45
  private
@@ -1,13 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "courrier/email/providers/base"
4
+ require "courrier/email/providers/inbox"
4
5
  require "courrier/email/providers/logger"
5
6
  require "courrier/email/providers/loops"
6
7
  require "courrier/email/providers/mailgun"
7
8
  require "courrier/email/providers/mailjet"
8
9
  require "courrier/email/providers/mailpace"
9
10
  require "courrier/email/providers/postmark"
10
- require "courrier/email/providers/preview"
11
+ require "courrier/email/providers/resend"
11
12
  require "courrier/email/providers/sendgrid"
12
13
  require "courrier/email/providers/sparkpost"
13
14
  require "courrier/email/providers/userlist"
@@ -15,11 +16,12 @@ require "courrier/email/providers/userlist"
15
16
  module Courrier
16
17
  class Email
17
18
  class Provider
18
- def initialize(provider: nil, api_key: nil, options: {}, provider_options: {})
19
+ def initialize(provider: nil, api_key: nil, options: {}, provider_options: {}, context_options: {})
19
20
  @provider = provider
20
21
  @api_key = api_key
21
22
  @options = options
22
23
  @provider_options = provider_options
24
+ @context_options = context_options
23
25
  end
24
26
 
25
27
  def deliver
@@ -29,20 +31,22 @@ module Courrier
29
31
  provider_class.new(
30
32
  api_key: @api_key,
31
33
  options: @options,
32
- provider_options: @provider_options
34
+ provider_options: @provider_options,
35
+ context_options: @context_options
33
36
  ).deliver
34
37
  end
35
38
 
36
39
  private
37
40
 
38
41
  PROVIDERS = {
42
+ inbox: Courrier::Email::Providers::Inbox,
39
43
  logger: Courrier::Email::Providers::Logger,
40
44
  loops: Courrier::Email::Providers::Loops,
41
45
  mailgun: Courrier::Email::Providers::Mailgun,
42
46
  mailjet: Courrier::Email::Providers::Mailjet,
43
47
  mailpace: Courrier::Email::Providers::Mailpace,
44
48
  postmark: Courrier::Email::Providers::Postmark,
45
- preview: Courrier::Email::Providers::Preview,
49
+ resend: Courrier::Email::Providers::Resend,
46
50
  sendgrid: Courrier::Email::Providers::Sendgrid,
47
51
  sparkpost: Courrier::Email::Providers::Sparkpost,
48
52
  userlist: Courrier::Email::Providers::Userlist
@@ -6,10 +6,11 @@ module Courrier
6
6
  class Email
7
7
  module Providers
8
8
  class Base
9
- def initialize(api_key: nil, options: {}, provider_options: {})
9
+ def initialize(api_key: nil, options: {}, provider_options: {}, context_options: {})
10
10
  @api_key = api_key
11
11
  @options = options
12
12
  @provider_options = provider_options
13
+ @context_options = context_options
13
14
  end
14
15
 
15
16
  def deliver
@@ -1,3 +1,10 @@
1
+ <!--
2
+ {
3
+ "to": "<%= email %>",
4
+ "subject": "<%= @options.subject %>"
5
+ }
6
+ -->
7
+
1
8
  <!DOCTYPE html>
2
9
  <html>
3
10
  <head>
@@ -6,7 +13,7 @@
6
13
  <style>
7
14
  *, *::before, *::after { box-sizing: border-box; }
8
15
  * { margin: 0; padding: 0; }
9
- body { margin: 0; padding: .5rem; line-height: 1.5; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; background-color: rgb(241, 245, 249); }
16
+ body { margin: 0; padding: 0; line-height: 1.5; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; background-color: rgb(241, 245, 249); }
10
17
  pre { margin: 0; white-space: pre-wrap; }
11
18
  .email {
12
19
  width: 100%;
@@ -14,15 +21,15 @@
14
21
  margin-left: auto; margin-right: auto;
15
22
  background-color: #fff;
16
23
  box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
17
- border-radius: 1rem;
24
+ border-bottom-right-radius: 1rem; border-bottom-left-radius: 1rem;
18
25
  }
19
26
 
20
- .email__header { display: flex; align-items: center; padding: 1rem .75rem; -moz-column-gap: .75rem; column-gap: .75rem; }
27
+ .email__header { display: flex; align-items: center; padding: .75rem; -moz-column-gap: .75rem; column-gap: .75rem; }
21
28
 
22
29
  .email__avatar {
23
30
  flex-shrink: 0;
24
31
  padding: .25rem;
25
- width: 2.5rem; height: 2.5rem;
32
+ width: 2rem; height: 2rem;
26
33
  background-color: rgb(203, 213, 225);
27
34
  color: rgb(51, 65, 85);
28
35
  border-radius: 9999px;
@@ -39,14 +46,14 @@
39
46
  .email__datetime { font-size: .75rem; line-height: 1rem; color: rgb(148, 163, 184); }
40
47
 
41
48
  .email__subject {
42
- margin-left: 4rem;
49
+ margin-left: 3.5rem;
43
50
  font-size: 1.25rem; line-height: 1.75rem;
44
51
  font-weight: 700;
45
52
  letter-spacing: -.025em;
46
53
  color: rgb(30, 41, 59)
47
54
  }
48
55
 
49
- .email__preview { margin-top: .75rem; margin-left: 4rem; padding: .5rem .25rem; }
56
+ .email__preview { margin-top: .75rem; margin-left: 3.5rem; padding-bottom: 1rem; }
50
57
 
51
58
  .preview-toggle { display: none; }
52
59
  .preview-toggle-label {
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tmpdir"
4
+ require "fileutils"
5
+ require "launchy"
6
+
7
+ module Courrier
8
+ class Email
9
+ module Providers
10
+ class Inbox < Base
11
+ def deliver
12
+ FileUtils.mkdir_p(config.destination)
13
+
14
+ file_path = File.join(config.destination, "#{Time.now.to_i}.html")
15
+
16
+ File.write(file_path, ERB.new(File.read(config.template_path)).result(binding))
17
+
18
+ Launchy.open(file_path) if config.auto_open
19
+
20
+ "📮 Email saved to #{file_path} and #{email_destination}"
21
+ end
22
+
23
+ def name = extract(@options.to)[:name]
24
+
25
+ def email = extract(@options.to)[:email]
26
+
27
+ def text = prepare(@options.text)
28
+
29
+ def html = prepare(@options.html)
30
+
31
+ private
32
+
33
+ def extract(to)
34
+ if to.to_s =~ /(.*?)\s*<(.+?)>/
35
+ {name: $1.strip, email: $2.strip}
36
+ else
37
+ {name: nil, email: to.to_s.strip}
38
+ end
39
+ end
40
+
41
+ def prepare(content)
42
+ content.to_s.gsub(URI::DEFAULT_PARSER.make_regexp(%w[http https])) do |url|
43
+ %(<a href="#{url}">#{url}</a>)
44
+ end
45
+ end
46
+
47
+ def config = @config ||= Courrier.configuration.inbox
48
+
49
+ def email_destination
50
+ return "opened in your default browser" if config.auto_open
51
+
52
+ path = begin
53
+ Rails.application.routes.url_helpers.courrier_path
54
+ rescue
55
+ "/courrier/ (Note: Add `mount Courrier::Engine => \"/courrier\"` to your routes.rb to enable proper routing)"
56
+ end
57
+
58
+ "available at #{path}"
59
+ end
60
+
61
+ class Email < Data.define(:path, :filename, :metadata)
62
+ Metadata = Data.define(:to, :subject)
63
+
64
+ def self.from_file(path)
65
+ content = File.read(path)
66
+ json = content[/<!--\s*(.*?)\s*-->/m, 1]
67
+ metadata = JSON.parse(json, symbolize_names: true)
68
+
69
+ new(
70
+ path: path,
71
+ filename: File.basename(path),
72
+ metadata: Metadata.new(**metadata)
73
+ )
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -22,14 +22,7 @@ module Courrier
22
22
  }
23
23
  end
24
24
 
25
- def data_variables
26
- {
27
- "from" => @options.from,
28
- "subject" => @options.subject,
29
- "text_content" => @options.text,
30
- "html_content" => @options.html
31
- }.compact
32
- end
25
+ def data_variables = @context_options.compact
33
26
  end
34
27
  end
35
28
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Courrier
4
+ class Email
5
+ module Providers
6
+ class Resend < Base
7
+ ENDPOINT_URL = "https://api.resend.com/emails"
8
+
9
+ def body
10
+ {
11
+ "from" => @options.from,
12
+ "to" => @options.to,
13
+ "reply_to" => @options.reply_to,
14
+ "cc" => @options.cc,
15
+ "bcc" => @options.bcc,
16
+ "subject" => @options.subject,
17
+ "text" => @options.text,
18
+ "html" => @options.html
19
+ }.compact
20
+ end
21
+
22
+ private
23
+
24
+ def headers
25
+ {
26
+ "Authorization" => "Bearer #{@api_key}"
27
+ }
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -79,7 +79,8 @@ module Courrier
79
79
  provider: @provider,
80
80
  api_key: @api_key,
81
81
  options: @options,
82
- provider_options: Courrier.configuration&.providers&.[](@provider.to_s.downcase.to_sym)
82
+ provider_options: Courrier.configuration&.providers&.[](@provider.to_s.downcase.to_sym),
83
+ context_options: @context_options
83
84
  ).deliver
84
85
  end
85
86
  alias_method :deliver_now, :deliver
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Courrier
4
+ class Engine < ::Rails::Engine
5
+ isolate_namespace Courrier
6
+ end
7
+ end
@@ -1,13 +1,13 @@
1
1
  namespace :tmp do
2
2
  task :courrier do
3
- rm_rf Dir["#{Courrier.configuration.preview.destination}/[^.]*"], verbose: false
3
+ rm_rf Dir["#{Courrier.configuration.inbox.destination}/[^.]*"], verbose: false
4
4
  end
5
5
 
6
6
  task clear: :courrier
7
7
  end
8
8
 
9
9
  namespace :courrier do
10
- desc "Clear preview email files from `#{Courrier.configuration.preview.destination}`"
10
+ desc "Clear email files from `#{Courrier.configuration.inbox.destination}`"
11
11
 
12
12
  task clear: "tmp:courrier"
13
13
  end
@@ -1,3 +1,3 @@
1
1
  module Courrier
2
- VERSION = "0.6.0"
2
+ VERSION = "0.7.0"
3
3
  end
data/lib/courrier.rb CHANGED
@@ -4,6 +4,7 @@ require "courrier/version"
4
4
  require "courrier/errors"
5
5
  require "courrier/configuration"
6
6
  require "courrier/email"
7
+ require "courrier/engine" if defined?(Rails)
7
8
  require "courrier/railtie" if defined?(Rails)
8
9
 
9
10
  module Courrier
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: courrier
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rails Designer
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-05-12 00:00:00.000000000 Z
11
+ date: 2025-05-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: launchy
@@ -50,8 +50,8 @@ dependencies:
50
50
  - - "<"
51
51
  - !ruby/object:Gem::Version
52
52
  version: '2'
53
- description: Modern, API-powered email delivery for Ruby apps with support for Postmark,
54
- SendGrid, Mailgun and more.
53
+ description: API-powered email delivery for Ruby apps with support for Postmark, SendGrid,
54
+ Mailgun and more.
55
55
  email:
56
56
  - devs@railsdesigner.com
57
57
  executables: []
@@ -62,13 +62,17 @@ files:
62
62
  - Gemfile.lock
63
63
  - README.md
64
64
  - Rakefile
65
+ - app/controllers/courrier/previews/cleanups_controller.rb
66
+ - app/controllers/courrier/previews_controller.rb
67
+ - app/views/courrier/previews/index.html.erb
65
68
  - bin/console
66
69
  - bin/release
67
70
  - bin/setup
71
+ - config/routes.rb
68
72
  - courrier.gemspec
69
73
  - lib/courrier.rb
70
74
  - lib/courrier/configuration.rb
71
- - lib/courrier/configuration/preview.rb
75
+ - lib/courrier/configuration/inbox.rb
72
76
  - lib/courrier/configuration/providers.rb
73
77
  - lib/courrier/email.rb
74
78
  - lib/courrier/email/address.rb
@@ -76,20 +80,22 @@ files:
76
80
  - lib/courrier/email/options.rb
77
81
  - lib/courrier/email/provider.rb
78
82
  - lib/courrier/email/providers/base.rb
83
+ - lib/courrier/email/providers/inbox.rb
84
+ - lib/courrier/email/providers/inbox/default.html.erb
79
85
  - lib/courrier/email/providers/logger.rb
80
86
  - lib/courrier/email/providers/loops.rb
81
87
  - lib/courrier/email/providers/mailgun.rb
82
88
  - lib/courrier/email/providers/mailjet.rb
83
89
  - lib/courrier/email/providers/mailpace.rb
84
90
  - lib/courrier/email/providers/postmark.rb
85
- - lib/courrier/email/providers/preview.rb
86
- - lib/courrier/email/providers/preview/default.html.erb
91
+ - lib/courrier/email/providers/resend.rb
87
92
  - lib/courrier/email/providers/sendgrid.rb
88
93
  - lib/courrier/email/providers/sparkpost.rb
89
94
  - lib/courrier/email/providers/userlist.rb
90
95
  - lib/courrier/email/request.rb
91
96
  - lib/courrier/email/result.rb
92
97
  - lib/courrier/email/transformer.rb
98
+ - lib/courrier/engine.rb
93
99
  - lib/courrier/errors.rb
94
100
  - lib/courrier/railtie.rb
95
101
  - lib/courrier/tasks/courrier.rake
@@ -122,5 +128,5 @@ requirements: []
122
128
  rubygems_version: 3.4.1
123
129
  signing_key:
124
130
  specification_version: 4
125
- summary: Modern, API-powered email delivery for Rails apps
131
+ summary: API-powered email delivery for Ruby apps
126
132
  test_files: []
@@ -1,51 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "tmpdir"
4
- require "fileutils"
5
- require "launchy"
6
-
7
- module Courrier
8
- class Email
9
- module Providers
10
- class Preview < Base
11
- def deliver
12
- FileUtils.mkdir_p(config.destination)
13
-
14
- file_path = File.join(config.destination, "#{Time.now.to_i}.html")
15
-
16
- File.write(file_path, ERB.new(File.read(config.template_path)).result(binding))
17
-
18
- Launchy.open(file_path)
19
-
20
- "Preview email saved to #{file_path}#{config.auto_open ? " and opened in browser" : ""}"
21
- end
22
-
23
- def name = extract(@options.to)[:name]
24
-
25
- def email = extract(@options.to)[:email]
26
-
27
- def text = prepare(@options.text)
28
-
29
- def html = prepare(@options.html)
30
-
31
- private
32
-
33
- def extract(to)
34
- if to.to_s =~ /(.*?)\s*<(.+?)>/
35
- {name: $1.strip, email: $2.strip}
36
- else
37
- {name: nil, email: to.to_s.strip}
38
- end
39
- end
40
-
41
- def prepare(content)
42
- content.to_s.gsub(URI::DEFAULT_PARSER.make_regexp(%w[http https])) do |url|
43
- %(<a href="#{url}">#{url}</a>)
44
- end
45
- end
46
-
47
- def config = @config ||= Courrier.configuration.preview
48
- end
49
- end
50
- end
51
- end