inbox_beam 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 37058947d959f74df792bb57612cc68eafe87a5eebcae8c9481fde1f1db31519
4
+ data.tar.gz: a7378b0b196ee61f516aab55869b66701180e3b84589d2c81aad4a8e09ea0761
5
+ SHA512:
6
+ metadata.gz: 79eff3ed7f9c5d519d78125b15a09d859270891109bdd3211bdeabc28c8a78ee21939be7c1600294bc72407880af1a07bbf7cee3d3bafb3b0cd204fe5b3f5cb6
7
+ data.tar.gz: 979eafb2c9a9f402bd74720625bde4914a99737a395cc77786842bf764c1d2474126d15822a76ecfcabc4acdce7aae2b869c1ccdfcd18b3dac53e44fc090de7b
data/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ - Initial release.
6
+ - `InboxBeam::Client`: append messages to a mailbox via IMAP `APPEND` (stdlib `net/imap`), app-password or OAuth2 (XOAUTH2) auth.
7
+ - `InboxBeam::Message`: dependency-free, UTF-8-safe RFC 5322 message builder (encoded-word headers, base64 bodies, `multipart/alternative` for html + text).
8
+ - `InboxBeam::DeliveryMethod`: drop-in Action Mailer delivery method (`config.action_mailer.delivery_method = :inbox_beam`), auto-registered via Railtie.
9
+ - Options: `mailbox`, `from`, `to`, `unread`, `subject_prefix`, per-call overrides.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 toyoshi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # inbox_beam
2
+
3
+ [![Gem Version](https://img.shields.io/gem/v/inbox_beam.svg)](https://rubygems.org/gems/inbox_beam)
4
+ [![license](https://img.shields.io/github/license/toyoshi/inbox_beam.svg)](./LICENSE)
5
+
6
+ **Send notification emails to your own inbox from Ruby and Rails without SMTP, a sending domain, or SPF/DKIM/DMARC setup.**
7
+
8
+ If all you want is for your Rails app to drop a contact-form copy, a sign-up
9
+ alert, or a cron failure into *your own* inbox, configuring SMTP, a sending
10
+ domain, SPF, DKIM, DMARC, and a provider like SES, SendGrid, or Postmark is a lot
11
+ of deliverability overhead for mail only you will read.
12
+
13
+ inbox_beam skips all of it. It uses the IMAP `APPEND` command to write the
14
+ message straight into your mailbox. No SMTP, no sending domain, no
15
+ deliverability — the email shows up unread and searchable, and nothing was sent.
16
+
17
+ It ships as a plain Ruby client and as a drop-in **Action Mailer delivery
18
+ method**, so your existing Rails mailers can land in your inbox unchanged.
19
+
20
+ ## Install
21
+
22
+ ```ruby
23
+ # Gemfile
24
+ gem "inbox_beam"
25
+ ```
26
+
27
+ ```sh
28
+ bundle install
29
+ # or
30
+ gem install inbox_beam
31
+ ```
32
+
33
+ Ruby >= 2.7.
34
+
35
+ ## Rails / Action Mailer
36
+
37
+ Point Action Mailer at the `:inbox_beam` delivery method. Every mailer you
38
+ already have now appends to your inbox instead of sending over SMTP:
39
+
40
+ ```ruby
41
+ # config/environments/production.rb
42
+ config.action_mailer.delivery_method = :inbox_beam
43
+ config.action_mailer.inbox_beam_settings = {
44
+ host: "imap.gmail.com",
45
+ auth: { user: "you@example.com", pass: ENV["IMAP_APP_PASSWORD"] },
46
+ mailbox: "INBOX"
47
+ }
48
+ ```
49
+
50
+ ```ruby
51
+ NotificationMailer.contact_form(submission).deliver_now
52
+ # → appended to your inbox. No SMTP, no SPF/DKIM/DMARC.
53
+ ```
54
+
55
+ The delivery method is registered automatically when Rails loads.
56
+
57
+ ## Plain Ruby
58
+
59
+ ```ruby
60
+ require "inbox_beam"
61
+
62
+ beam = InboxBeam::Client.new(
63
+ host: "imap.gmail.com",
64
+ auth: { user: "you@example.com", pass: ENV["IMAP_APP_PASSWORD"] }
65
+ )
66
+
67
+ beam.beam(subject: "New contact", text: "someone submitted the form")
68
+ ```
69
+
70
+ HTML plus text produces a `multipart/alternative` message:
71
+
72
+ ```ruby
73
+ beam.beam(
74
+ subject: "Weekly report",
75
+ text: "Signups: 42",
76
+ html: "<h1>Weekly report</h1><p>Signups: 42</p>"
77
+ )
78
+ ```
79
+
80
+ Override the target mailbox per call:
81
+
82
+ ```ruby
83
+ beam.beam(subject: "Cron failed", text: "nightly-export exited 1", mailbox: "Alerts")
84
+ ```
85
+
86
+ ## This is not email delivery
87
+
88
+ inbox_beam appends to a mailbox you already control. It does not send mail and
89
+ cannot reach anyone else. To email a user or customer, use SMTP or a delivery
90
+ API. It keeps no send log and the date is set by the client, so don't use it
91
+ where an audit trail matters. It's for your own notifications.
92
+
93
+ ## Gmail setup
94
+
95
+ 1. Enable IMAP in Gmail settings.
96
+ 2. Turn on 2-Step Verification and create an [App Password](https://myaccount.google.com/apppasswords).
97
+ 3. Use it as `auth[:pass]`.
98
+
99
+ Workspace admins can disable app passwords. In that case pass an OAuth2 token as
100
+ `auth[:access_token]` instead of `pass` (the client authenticates with XOAUTH2).
101
+ A dedicated `notify@` account that forwards to you is safer than your primary
102
+ account's credentials on a server.
103
+
104
+ ## API
105
+
106
+ ### `InboxBeam::Client.new(...)`
107
+
108
+ | Option | Default | Notes |
109
+ | --- | --- | --- |
110
+ | `host:` | — | IMAP host, e.g. `imap.gmail.com`. |
111
+ | `auth:` | — | `{ user:, pass: }` (app password) or `{ user:, access_token: }` (OAuth2). |
112
+ | `port:` | `993` | |
113
+ | `ssl:` | `true` | Use TLS. |
114
+ | `mailbox:` | `"INBOX"` | Target mailbox or label. |
115
+ | `from:` | `auth[:user]` | Default From. |
116
+ | `to:` | `auth[:user]` | Default To. |
117
+ | `unread:` | `true` | Leave appended messages unread. |
118
+ | `subject_prefix:` | `nil` | Prepended to every subject. |
119
+
120
+ ### `client.beam(...)`
121
+
122
+ Keyword args: `subject:` (required), `text:`, `html:`, `from:`, `to:`,
123
+ `mailbox:`, `unread:`, `date:`. Per-call values override the constructor
124
+ defaults. Returns an `InboxBeam::Result` with `mailbox`, `uid`, and
125
+ `uid_validity` (uid comes from the server's `APPENDUID` response).
126
+
127
+ ### `InboxBeam::Message.build(...)`
128
+
129
+ The RFC 5322 builder is available on its own if you want the raw message without
130
+ the IMAP connection. Zero dependencies, UTF-8 safe.
131
+
132
+ ```ruby
133
+ raw = InboxBeam::Message.build(from: "a@x.com", to: "b@x.com", subject: "Hi", text: "Body")
134
+ ```
135
+
136
+ ## How it works
137
+
138
+ IMAP's `APPEND` command (RFC 9051 §6.3.12) adds a message to the end of a
139
+ mailbox. It's the same command mail clients use to save sent copies and drafts.
140
+ inbox_beam builds an RFC 5322 message and appends it over a TLS IMAP connection
141
+ using Ruby's standard `net/imap` — no runtime dependencies of its own.
142
+
143
+ A TypeScript/Node version is at
144
+ [inbox-beam](https://github.com/toyoshi/inbox-beam).
145
+
146
+ ## License
147
+
148
+ MIT
@@ -0,0 +1,97 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/imap"
4
+ require_relative "message"
5
+
6
+ module InboxBeam
7
+ Result = Struct.new(:mailbox, :uid, :uid_validity, keyword_init: true)
8
+
9
+ # Writes a message directly into your own mailbox using IMAP APPEND.
10
+ #
11
+ # This is NOT email delivery. Nothing is sent over SMTP and no message leaves
12
+ # for another server — it is appended straight into the target mailbox. Use it
13
+ # to land your own app notifications in your own inbox without SPF/DKIM/DMARC.
14
+ class Client
15
+ DEFAULT_PORT = 993
16
+ DEFAULT_MAILBOX = "INBOX"
17
+
18
+ # @param host [String] IMAP host, e.g. "imap.gmail.com"
19
+ # @param auth [Hash] { user:, pass: } for an app password, or
20
+ # { user:, access_token: } for OAuth2 (XOAUTH2)
21
+ # @param port [Integer]
22
+ # @param ssl [Boolean] use TLS
23
+ # @param mailbox [String] target mailbox / label
24
+ # @param from [String, nil] default From (defaults to auth[:user])
25
+ # @param to [String, nil] default To (defaults to auth[:user])
26
+ # @param unread [Boolean] leave appended messages unread
27
+ # @param subject_prefix [String, nil] prepended to every subject
28
+ def initialize(host:, auth:, port: DEFAULT_PORT, ssl: true,
29
+ mailbox: DEFAULT_MAILBOX, from: nil, to: nil,
30
+ unread: true, subject_prefix: nil)
31
+ @host = host
32
+ @auth = auth
33
+ @port = port
34
+ @ssl = ssl
35
+ @mailbox = mailbox
36
+ @from = from
37
+ @to = to
38
+ @unread = unread
39
+ @subject_prefix = subject_prefix
40
+ end
41
+
42
+ # Append one message to the mailbox.
43
+ # @return [InboxBeam::Result]
44
+ def beam(subject:, text: nil, html: nil, from: nil, to: nil,
45
+ mailbox: nil, unread: nil, date: nil)
46
+ to ||= @to || @auth[:user]
47
+ from ||= @from || to
48
+ mailbox ||= @mailbox
49
+ unread = @unread if unread.nil?
50
+ subject = "#{@subject_prefix} #{subject}" if @subject_prefix
51
+
52
+ raw = Message.build(from: from, to: to, subject: subject, text: text, html: html, date: date)
53
+ append(mailbox, raw, unread: unread)
54
+ end
55
+
56
+ # Append an already-encoded RFC 5322 message (e.g. Mail::Message#encoded).
57
+ # @return [InboxBeam::Result]
58
+ def append(mailbox, raw, unread: true)
59
+ flags = unread ? [] : [:Seen]
60
+ response = with_connection { |imap| imap.append(mailbox, raw, flags, nil) }
61
+ uid_validity, uid = append_uid(response)
62
+ Result.new(mailbox: mailbox, uid: uid, uid_validity: uid_validity)
63
+ end
64
+
65
+ private
66
+
67
+ def with_connection
68
+ imap = Net::IMAP.new(@host, port: @port, ssl: @ssl)
69
+ begin
70
+ if @auth[:access_token]
71
+ imap.authenticate("XOAUTH2", @auth[:user], @auth[:access_token])
72
+ else
73
+ imap.login(@auth[:user], @auth[:pass])
74
+ end
75
+ yield imap
76
+ ensure
77
+ begin
78
+ imap.logout
79
+ rescue StandardError
80
+ nil
81
+ end
82
+ imap.disconnect
83
+ end
84
+ end
85
+
86
+ # Parse the APPENDUID response code: [APPENDUID <uidvalidity> <uid>].
87
+ def append_uid(response)
88
+ code = response&.data&.code
89
+ return [nil, nil] unless code && code.name == "APPENDUID"
90
+
91
+ uid_validity, uid = code.data.to_s.split(" ", 2)
92
+ [uid_validity&.to_i, uid&.to_i]
93
+ rescue StandardError
94
+ [nil, nil]
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "client"
4
+
5
+ module InboxBeam
6
+ # Action Mailer delivery method. Instead of sending over SMTP, it appends the
7
+ # rendered message straight into your own mailbox via IMAP APPEND.
8
+ #
9
+ # config.action_mailer.delivery_method = :inbox_beam
10
+ # config.action_mailer.inbox_beam_settings = {
11
+ # host: "imap.gmail.com",
12
+ # auth: { user: "you@example.com", pass: ENV["IMAP_APP_PASSWORD"] },
13
+ # mailbox: "INBOX"
14
+ # }
15
+ #
16
+ # Existing mailers then land in your inbox with no sending domain, SPF, DKIM,
17
+ # or DMARC. For notifications you read yourself — not for mailing third parties.
18
+ class DeliveryMethod
19
+ attr_reader :settings
20
+
21
+ def initialize(settings = {})
22
+ @settings = settings
23
+ end
24
+
25
+ # @param mail [Mail::Message]
26
+ def deliver!(mail)
27
+ opts = settings.dup
28
+ mailbox = opts.delete(:mailbox) || Client::DEFAULT_MAILBOX
29
+ unread = opts.key?(:unread) ? opts.delete(:unread) : true
30
+ Client.new(**opts).append(mailbox, mail.encoded, unread: unread)
31
+ mail
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "base64"
4
+ require "securerandom"
5
+
6
+ module InboxBeam
7
+ # Builds an RFC 5322 message ready to hand to IMAP APPEND.
8
+ # Zero dependencies — UTF-8 safe via base64 bodies and encoded-word headers.
9
+ module Message
10
+ CRLF = "\r\n"
11
+
12
+ module_function
13
+
14
+ # @param from [String] e.g. "you@example.com" or "Name <you@example.com>"
15
+ # @param to [String]
16
+ # @param subject [String]
17
+ # @param text [String, nil]
18
+ # @param html [String, nil]
19
+ # @param date [Time, nil]
20
+ # @return [String] the raw RFC 5322 message with CRLF line endings
21
+ def build(from:, to:, subject:, text: nil, html: nil, date: nil)
22
+ date ||= Time.now
23
+ message_id = "<#{SecureRandom.hex(16)}@#{domain_of(from)}>"
24
+
25
+ headers = [
26
+ "From: #{encode_header(from)}",
27
+ "To: #{encode_header(to)}",
28
+ "Subject: #{encode_word(subject)}",
29
+ "Date: #{rfc2822_date(date)}",
30
+ "Message-ID: #{message_id}",
31
+ "MIME-Version: 1.0"
32
+ ]
33
+
34
+ if html && text
35
+ boundary = "inbox_beam_#{SecureRandom.hex(12)}"
36
+ headers << %(Content-Type: multipart/alternative; boundary="#{boundary}")
37
+ body = [
38
+ "--#{boundary}",
39
+ mime_part("text/plain", text),
40
+ "--#{boundary}",
41
+ mime_part("text/html", html),
42
+ "--#{boundary}--",
43
+ ""
44
+ ].join(CRLF)
45
+ elsif html
46
+ headers << "Content-Type: text/html; charset=UTF-8" << "Content-Transfer-Encoding: base64"
47
+ body = base64_body(html)
48
+ else
49
+ headers << "Content-Type: text/plain; charset=UTF-8" << "Content-Transfer-Encoding: base64"
50
+ body = base64_body(text.to_s)
51
+ end
52
+
53
+ raw = headers.join(CRLF) + CRLF + CRLF + body
54
+ raw.gsub(/\r\n|\r|\n/, CRLF)
55
+ end
56
+
57
+ def ascii?(value)
58
+ value.ascii_only?
59
+ end
60
+
61
+ # RFC 2047 encoded-word for non-ASCII header values.
62
+ def encode_word(value)
63
+ return value if ascii?(value)
64
+
65
+ "=?UTF-8?B?#{Base64.strict_encode64(value.encode("UTF-8"))}?="
66
+ end
67
+
68
+ # Encode a header that may contain a display name: `名前 <addr>`.
69
+ def encode_header(value)
70
+ return value if ascii?(value)
71
+
72
+ if (m = value.match(/\A(.*?)\s*<([^>]+)>\s*\z/))
73
+ "#{encode_word(m[1])} <#{m[2]}>"
74
+ else
75
+ encode_word(value)
76
+ end
77
+ end
78
+
79
+ # Base64-encode a body and wrap at 76 columns per RFC 2045.
80
+ def base64_body(content)
81
+ Base64.strict_encode64(content.encode("UTF-8")).scan(/.{1,76}/).join(CRLF)
82
+ end
83
+
84
+ def rfc2822_date(time)
85
+ time.getutc.strftime("%a, %d %b %Y %H:%M:%S +0000")
86
+ end
87
+
88
+ def domain_of(address)
89
+ m = address.match(/@([^>\s]+)/)
90
+ m ? m[1] : "localhost"
91
+ end
92
+
93
+ def mime_part(content_type, content)
94
+ [
95
+ "Content-Type: #{content_type}; charset=UTF-8",
96
+ "Content-Transfer-Encoding: base64",
97
+ "",
98
+ base64_body(content)
99
+ ].join(CRLF)
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/railtie"
4
+ require_relative "delivery_method"
5
+
6
+ module InboxBeam
7
+ # Registers the :inbox_beam Action Mailer delivery method when Rails is loaded.
8
+ class Railtie < Rails::Railtie
9
+ initializer "inbox_beam.add_delivery_method" do
10
+ ActiveSupport.on_load(:action_mailer) do
11
+ add_delivery_method(:inbox_beam, InboxBeam::DeliveryMethod)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module InboxBeam
4
+ VERSION = "0.1.0"
5
+ end
data/lib/inbox_beam.rb ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "inbox_beam/version"
4
+ require_relative "inbox_beam/message"
5
+ require_relative "inbox_beam/client"
6
+ require_relative "inbox_beam/delivery_method"
7
+
8
+ # InboxBeam puts notifications into your own mailbox via IMAP APPEND —
9
+ # no SMTP, no sending domain, no SPF/DKIM/DMARC.
10
+ module InboxBeam
11
+ end
12
+
13
+ # Load the Action Mailer integration only when Rails is present.
14
+ require_relative "inbox_beam/railtie" if defined?(Rails::Railtie)
metadata ADDED
@@ -0,0 +1,73 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: inbox_beam
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - toyoshi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-06-08 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: net-imap
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.2'
27
+ description: InboxBeam appends a message straight into your own mailbox using the
28
+ IMAP APPEND command instead of sending email. Useful for landing app notifications,
29
+ contact-form copies, and cron alerts in your own inbox without standing up a sending
30
+ domain. Includes a drop-in Action Mailer delivery method.
31
+ email:
32
+ - toyoshi@tokuiten.jp
33
+ executables: []
34
+ extensions: []
35
+ extra_rdoc_files: []
36
+ files:
37
+ - CHANGELOG.md
38
+ - LICENSE
39
+ - README.md
40
+ - lib/inbox_beam.rb
41
+ - lib/inbox_beam/client.rb
42
+ - lib/inbox_beam/delivery_method.rb
43
+ - lib/inbox_beam/message.rb
44
+ - lib/inbox_beam/railtie.rb
45
+ - lib/inbox_beam/version.rb
46
+ homepage: https://github.com/toyoshi/inbox_beam
47
+ licenses:
48
+ - MIT
49
+ metadata:
50
+ homepage_uri: https://github.com/toyoshi/inbox_beam
51
+ source_code_uri: https://github.com/toyoshi/inbox_beam
52
+ changelog_uri: https://github.com/toyoshi/inbox_beam/blob/main/CHANGELOG.md
53
+ rubygems_mfa_required: 'true'
54
+ post_install_message:
55
+ rdoc_options: []
56
+ require_paths:
57
+ - lib
58
+ required_ruby_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: 2.7.0
63
+ required_rubygems_version: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ requirements: []
69
+ rubygems_version: 3.0.3.1
70
+ signing_key:
71
+ specification_version: 4
72
+ summary: Put notifications into your own mailbox via IMAP APPEND — no SMTP, no SPF/DKIM/DMARC.
73
+ test_files: []