courrier 0.5.1 → 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 +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +20 -8
- data/app/controllers/courrier/previews/cleanups_controller.rb +13 -0
- data/app/controllers/courrier/previews_controller.rb +23 -0
- data/app/views/courrier/previews/index.html.erb +171 -0
- data/config/routes.rb +8 -0
- data/courrier.gemspec +3 -3
- data/lib/courrier/configuration/{preview.rb → inbox.rb} +3 -3
- data/lib/courrier/configuration.rb +3 -3
- data/lib/courrier/email/provider.rb +11 -5
- data/lib/courrier/email/providers/base.rb +2 -1
- data/lib/courrier/email/providers/{preview → inbox}/default.html.erb +13 -6
- data/lib/courrier/email/providers/inbox.rb +79 -0
- data/lib/courrier/email/providers/loops.rb +1 -8
- data/lib/courrier/email/providers/mailgun.rb +5 -1
- data/lib/courrier/email/providers/resend.rb +32 -0
- data/lib/courrier/email/providers/userlist.rb +66 -0
- data/lib/courrier/email.rb +2 -1
- data/lib/courrier/engine.rb +7 -0
- data/lib/courrier/tasks/courrier.rake +2 -2
- data/lib/courrier/version.rb +1 -1
- data/lib/courrier.rb +1 -0
- metadata +15 -8
- data/lib/courrier/email/providers/preview.rb +0 -51
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1359782140920863fe902186544d8b34bcbc513ca4412b26bc3bea975196916e
|
4
|
+
data.tar.gz: 493af39e04f09434689cb810a57c591b02ec9373b84660de4de0349c168eebfb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d7928a0eb201e6b4b5e36612a03641c49a862477c6338be08800e10c1a4b9cc5f8e1917253a2a2b9b414c51ef361c9dcbfe87a309e1fcba22fb8af43310f068e
|
7
|
+
data.tar.gz: 9a4c22a760d6c9af901beaa076a30dcbe02db45b2c339932bef2a3e6df90531d3bdaf860c86bb954d0397243b73c4b2b07dd4072eef6cd5c9d641a9e2f3303fd
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
# Courrier
|
2
2
|
|
3
|
-
|
3
|
+
API-powered email delivery for Ruby apps.
|
4
4
|
|
5
|
-

|
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"
|
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,25 +160,36 @@ 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)
|
166
|
+
- [Userlist](https://userlist.com)
|
164
167
|
|
165
168
|
⚠️ Some providers still need manual verification of their implementation. If you're using one of these providers, please help verify the implementation by sharing your experience in [this GitHub issue](https://github.com/Rails-Designer/courrier/issues/4). 🙏
|
166
169
|
|
167
170
|
|
168
171
|
## More Features
|
169
172
|
|
170
|
-
Additional functionality to help with development and
|
173
|
+
Additional functionality to help with development and testing:
|
174
|
+
|
171
175
|
|
176
|
+
### Inbox (Rails only)
|
172
177
|
|
173
|
-
|
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
|
+
```
|
174
185
|
|
175
|
-
|
186
|
+
If you want to automatically open every email in your default browser:
|
176
187
|
```ruby
|
177
|
-
config.provider = "
|
188
|
+
config.provider = "inbox"
|
189
|
+
config.inbox.auto_open = true
|
178
190
|
```
|
179
191
|
|
180
|
-
|
192
|
+
Emails are automatically cleared with `bin/rails tmp:clear`, or manually with `bin/rails courrier:clear`.
|
181
193
|
|
182
194
|
|
183
195
|
### Layout Support
|
@@ -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
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 = "
|
12
|
-
spec.description = "
|
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,
|
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
|
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 =
|
11
|
-
@template_path = File.expand_path("../../../courrier/email/providers/
|
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/
|
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, :
|
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
|
-
@
|
42
|
+
@inbox = Courrier::Configuration::Inbox.new
|
43
43
|
end
|
44
44
|
|
45
45
|
private
|
@@ -1,24 +1,27 @@
|
|
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/
|
11
|
+
require "courrier/email/providers/resend"
|
11
12
|
require "courrier/email/providers/sendgrid"
|
12
13
|
require "courrier/email/providers/sparkpost"
|
14
|
+
require "courrier/email/providers/userlist"
|
13
15
|
|
14
16
|
module Courrier
|
15
17
|
class Email
|
16
18
|
class Provider
|
17
|
-
def initialize(provider: nil, api_key: nil, options: {}, provider_options: {})
|
19
|
+
def initialize(provider: nil, api_key: nil, options: {}, provider_options: {}, context_options: {})
|
18
20
|
@provider = provider
|
19
21
|
@api_key = api_key
|
20
22
|
@options = options
|
21
23
|
@provider_options = provider_options
|
24
|
+
@context_options = context_options
|
22
25
|
end
|
23
26
|
|
24
27
|
def deliver
|
@@ -28,22 +31,25 @@ module Courrier
|
|
28
31
|
provider_class.new(
|
29
32
|
api_key: @api_key,
|
30
33
|
options: @options,
|
31
|
-
provider_options: @provider_options
|
34
|
+
provider_options: @provider_options,
|
35
|
+
context_options: @context_options
|
32
36
|
).deliver
|
33
37
|
end
|
34
38
|
|
35
39
|
private
|
36
40
|
|
37
41
|
PROVIDERS = {
|
42
|
+
inbox: Courrier::Email::Providers::Inbox,
|
38
43
|
logger: Courrier::Email::Providers::Logger,
|
39
44
|
loops: Courrier::Email::Providers::Loops,
|
40
45
|
mailgun: Courrier::Email::Providers::Mailgun,
|
41
46
|
mailjet: Courrier::Email::Providers::Mailjet,
|
42
47
|
mailpace: Courrier::Email::Providers::Mailpace,
|
43
48
|
postmark: Courrier::Email::Providers::Postmark,
|
44
|
-
|
49
|
+
resend: Courrier::Email::Providers::Resend,
|
45
50
|
sendgrid: Courrier::Email::Providers::Sendgrid,
|
46
|
-
sparkpost: Courrier::Email::Providers::Sparkpost
|
51
|
+
sparkpost: Courrier::Email::Providers::Sparkpost,
|
52
|
+
userlist: Courrier::Email::Providers::Userlist
|
47
53
|
}.freeze
|
48
54
|
|
49
55
|
def configuration_missing_in_production?
|
@@ -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:
|
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:
|
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:
|
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:
|
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:
|
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
|
@@ -29,7 +29,11 @@ module Courrier
|
|
29
29
|
|
30
30
|
def content_type = "multipart/form-data"
|
31
31
|
|
32
|
-
def
|
32
|
+
def headers
|
33
|
+
{
|
34
|
+
"Authorization" => "Basic #{Base64.strict_encode64("api:#{@api_key}")}"
|
35
|
+
}
|
36
|
+
end
|
33
37
|
end
|
34
38
|
end
|
35
39
|
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
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Courrier
|
4
|
+
class Email
|
5
|
+
module Providers
|
6
|
+
class Userlist < Base
|
7
|
+
ENDPOINT_URL = "https://push.userlist.com/messages"
|
8
|
+
|
9
|
+
def body
|
10
|
+
{
|
11
|
+
"from" => @options.from,
|
12
|
+
"to" => @options.to,
|
13
|
+
"subject" => @options.subject,
|
14
|
+
"body" => body_document
|
15
|
+
}.compact.merge(provider_options)
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def headers
|
21
|
+
{
|
22
|
+
"Authorization" => "Push #{@api_key}"
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def body_document
|
27
|
+
if @options.html && @options.text
|
28
|
+
multipart_document
|
29
|
+
elsif @options.html
|
30
|
+
html_document
|
31
|
+
elsif @options.text
|
32
|
+
text_document
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def text_document
|
37
|
+
{
|
38
|
+
"type" => "text",
|
39
|
+
"content" => @options.text
|
40
|
+
}
|
41
|
+
end
|
42
|
+
|
43
|
+
def html_document
|
44
|
+
{
|
45
|
+
"type" => "html",
|
46
|
+
"content" => @options.html
|
47
|
+
}
|
48
|
+
end
|
49
|
+
|
50
|
+
def multipart_document
|
51
|
+
{
|
52
|
+
"type" => "multipart",
|
53
|
+
"content" => [
|
54
|
+
html_document,
|
55
|
+
text_document
|
56
|
+
]
|
57
|
+
}
|
58
|
+
end
|
59
|
+
|
60
|
+
def provider_options
|
61
|
+
{"theme" => nil}.merge(@provider_options)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/courrier/email.rb
CHANGED
@@ -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
|
@@ -1,13 +1,13 @@
|
|
1
1
|
namespace :tmp do
|
2
2
|
task :courrier do
|
3
|
-
rm_rf Dir["#{Courrier.configuration.
|
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
|
10
|
+
desc "Clear email files from `#{Courrier.configuration.inbox.destination}`"
|
11
11
|
|
12
12
|
task clear: "tmp:courrier"
|
13
13
|
end
|
data/lib/courrier/version.rb
CHANGED
data/lib/courrier.rb
CHANGED
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.
|
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-
|
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:
|
54
|
-
|
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/
|
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,19 +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/
|
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
|
94
|
+
- lib/courrier/email/providers/userlist.rb
|
89
95
|
- lib/courrier/email/request.rb
|
90
96
|
- lib/courrier/email/result.rb
|
91
97
|
- lib/courrier/email/transformer.rb
|
98
|
+
- lib/courrier/engine.rb
|
92
99
|
- lib/courrier/errors.rb
|
93
100
|
- lib/courrier/railtie.rb
|
94
101
|
- lib/courrier/tasks/courrier.rake
|
@@ -121,5 +128,5 @@ requirements: []
|
|
121
128
|
rubygems_version: 3.4.1
|
122
129
|
signing_key:
|
123
130
|
specification_version: 4
|
124
|
-
summary:
|
131
|
+
summary: API-powered email delivery for Ruby apps
|
125
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
|