mailsmtp 0.2.1 → 0.3.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/CHANGELOG.md +11 -1
- data/README.md +2 -2
- data/lib/mailsmtp/data_reader.rb +1 -1
- data/lib/mailsmtp/error.rb +5 -20
- data/lib/mailsmtp/errors.rb +6 -0
- data/lib/mailsmtp/server.rb +139 -16
- data/lib/mailsmtp/session.rb +3 -1
- data/lib/mailsmtp/version.rb +1 -1
- data/mailsmtp.gemspec +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 57ea364c7b9a8ef21f22f61233d07a6c6f765f097351765d51fe807dea5dd66a
|
|
4
|
+
data.tar.gz: b5d59eb1ee5152050d0b3f79001263f5ab1632c46acdc8dbd3a6f99489b07ddf
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f7319c0c6aeaa73664bd5e47317a6f4bdb3ce64667ae417861da5a32983afebee8824cc3256c0e2f8357713788ec18c3444410d10ff76c873691cd2b5e379fb2
|
|
7
|
+
data.tar.gz: cee5dfc590c8ec1dc0c00661c84bba351bb0acc3dabb47e5965b81c805e19a965145c0344e79e659d4c2a08429fe5956f6bf4cfb9829de7c37281fd963c52921
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
- Refuse a SASL PLAIN authzid the application did not grant, and reject identities containing CR or LF
|
|
6
|
+
- Only a String or Symbol returned from `authenticate` now sets `session.authorization_id`
|
|
7
|
+
- Neuter non-printables and RFC 5322 header specials in the HELO/EHLO name
|
|
8
|
+
- Added `MailSmtp::ConnectionLost` so a peer hang-up logs as a disconnect, not an error
|
|
9
|
+
- Require the PROXY preamble before any other command from a peer in `proxy_hosts`
|
|
10
|
+
- Read `MAIL FROM SIZE=` as decimal, so `SIZE=08` no longer raises and `SIZE=0100` is not octal
|
|
11
|
+
- Validate `MailSmtp::Error` subclasses missing `status` on `#response`, not at definition
|
|
12
|
+
|
|
3
13
|
## 0.2.1
|
|
4
14
|
|
|
5
15
|
Documentation
|
|
@@ -23,4 +33,4 @@ Documentation
|
|
|
23
33
|
- Added PROXY protocol v1 support
|
|
24
34
|
- Added TLS hot-swap via `#tls=`
|
|
25
35
|
- Added `session.tls_version` and `session.tls_cipher` after STARTTLS
|
|
26
|
-
- Declared `base64` and `logger` as runtime dependencies
|
|
36
|
+
- Declared `base64` and `logger` as runtime dependencies
|
data/README.md
CHANGED
|
@@ -63,7 +63,7 @@ class MyServer < MailSmtp::Server
|
|
|
63
63
|
|
|
64
64
|
def authenticate(session, username:, password:, authorization_id: "")
|
|
65
65
|
raise MailSmtp::AuthenticationFailed unless username == "user" && password == "secret"
|
|
66
|
-
username #
|
|
66
|
+
username # granted as session.authorization_id
|
|
67
67
|
end
|
|
68
68
|
|
|
69
69
|
def mail_from(session, address)
|
|
@@ -146,7 +146,7 @@ Raising a `MailSmtp::Error` subclass sends that SMTP reply to the client. Other
|
|
|
146
146
|
| `disconnected` | `(session)` | — |
|
|
147
147
|
| `helo` | `(session, name)` | after STARTTLS, `session.tls_version` / `session.tls_cipher` are set |
|
|
148
148
|
| `proxy` | `(session, proxy_data)` | hash replaces PROXY data |
|
|
149
|
-
| `authenticate` | `(session, username:, password:, authorization_id:)` |
|
|
149
|
+
| `authenticate` | `(session, username:, password:, authorization_id:)` | returned String grants that `authorization_id`, else the username is used; raise `AuthenticationFailed` |
|
|
150
150
|
| `auth_mechanisms` | `(session)` | |
|
|
151
151
|
| `mail_from` | `(session, address)` | returned string is stored as envelope from |
|
|
152
152
|
| `rcpt_to` | `(session, address)` | returned string is appended to envelope to |
|
data/lib/mailsmtp/data_reader.rb
CHANGED
|
@@ -87,7 +87,7 @@ module MailSmtp
|
|
|
87
87
|
# like a clean end-of-message (apps that rescue EOFError would accept a
|
|
88
88
|
# truncated body and the engine would map that to LocalError / 451).
|
|
89
89
|
raise EOFError, "end of data" if @eof
|
|
90
|
-
raise
|
|
90
|
+
raise ConnectionLost, "Connection lost during DATA"
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
chunk = @buffer.slice!(0, maxlen)
|
data/lib/mailsmtp/error.rb
CHANGED
|
@@ -8,26 +8,11 @@ module MailSmtp
|
|
|
8
8
|
@enhanced = enhanced
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
next unless tp.method_id == :initialize &&
|
|
17
|
-
tp.defined_class == Class &&
|
|
18
|
-
tp.return_value.equal?(subclass)
|
|
19
|
-
tp.disable
|
|
20
|
-
subclass.assert_status!
|
|
21
|
-
end
|
|
22
|
-
else
|
|
23
|
-
TracePoint.trace(:end) do |tp|
|
|
24
|
-
next unless tp.self.equal?(subclass)
|
|
25
|
-
tp.disable
|
|
26
|
-
subclass.assert_status!
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
11
|
+
# A missing status is caught by +#response+, the only place the values are
|
|
12
|
+
# ever needed. Validating it eagerly at definition time instead would mean
|
|
13
|
+
# enabling a process-global TracePoint on every subclass — which traces
|
|
14
|
+
# every thread, and is stranded permanently if the class body raises before
|
|
15
|
+
# the matching event arrives.
|
|
31
16
|
def assert_status!
|
|
32
17
|
if code.nil? || text.nil? || enhanced.nil?
|
|
33
18
|
raise "MailSmtp::Error subclass #{self} must declare status"
|
data/lib/mailsmtp/errors.rb
CHANGED
|
@@ -8,6 +8,12 @@ module MailSmtp
|
|
|
8
8
|
status 421, "Service too busy or not available, closing transmission channel", enhanced: "4.3.2"
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
+
# 421 — the peer is already gone before any reply could reach it, e.g. a
|
|
12
|
+
# mid-DATA disconnect. Subclasses +ServiceUnavailable+ so existing rescues
|
|
13
|
+
# still catch it, but the engine logs it as a routine disconnect, not an error.
|
|
14
|
+
class ConnectionLost < ServiceUnavailable
|
|
15
|
+
end
|
|
16
|
+
|
|
11
17
|
# 451 — the app hit an unexpected error handling an otherwise-valid message
|
|
12
18
|
class LocalError < Error
|
|
13
19
|
status 451, "Requested action aborted: local error in processing", enhanced: "4.3.0"
|
data/lib/mailsmtp/server.rb
CHANGED
|
@@ -54,6 +54,25 @@ module MailSmtp
|
|
|
54
54
|
# up to 12288 octets — not the 512-octet SMTP command-line ceiling.
|
|
55
55
|
MAX_AUTH_LINE_LENGTH = 12_288
|
|
56
56
|
|
|
57
|
+
# RFC 5322 header specials: parens (comments), backslash/quote (quoted
|
|
58
|
+
# strings), angle brackets and "@" (addr-spec), ";" (Received date
|
|
59
|
+
# delimiter), space (token boundary). Postfix neuters exactly this set.
|
|
60
|
+
HELO_SPECIALS = ' <>()\\";@'.freeze
|
|
61
|
+
# Everything outside printable US-ASCII, plus HELO_SPECIALS. Not
|
|
62
|
+
# fixed-encoding, so it applies to a BINARY subject byte by byte.
|
|
63
|
+
HELO_UNSAFE = /[^\x20-\x7e]|[#{Regexp.escape(HELO_SPECIALS)}]/
|
|
64
|
+
# For an SMTPUTF8 HELO: well-formed UTF-8 survives except control/format
|
|
65
|
+
# characters (\p{Cc}\p{Cf} — bidi overrides, zero-width joiners), non-ASCII
|
|
66
|
+
# spaces and line separators (\p{Zs}\p{Zl}\p{Zp} — could smuggle whitespace
|
|
67
|
+
# or newlines past the ASCII rule), and unassigned/private-use codepoints
|
|
68
|
+
# (\p{Cn}\p{Co}).
|
|
69
|
+
#
|
|
70
|
+
# Fixed-encoding UTF-8, so unlike HELO_UNSAFE it raises
|
|
71
|
+
# Encoding::CompatibilityError on a BINARY subject with high bytes — only
|
|
72
|
+
# apply it to a valid_encoding? UTF-8 string (see +neuter_helo+).
|
|
73
|
+
HELO_UNSAFE_UTF8 =
|
|
74
|
+
/[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\p{Zs}\p{Cn}\p{Co}]|[#{Regexp.escape(HELO_SPECIALS)}]/
|
|
75
|
+
|
|
57
76
|
MAX_FAILED_ACCEPTS = 10
|
|
58
77
|
FAILED_ACCEPT_BACKOFF = 0.5
|
|
59
78
|
|
|
@@ -194,7 +213,8 @@ module MailSmtp
|
|
|
194
213
|
# Override to customize the opt-in Received header (+add_received: true+).
|
|
195
214
|
def received_header(session)
|
|
196
215
|
protocol = session.encrypted? ? (session.authenticated? ? "ESMTPSA" : "ESMTPS") : "ESMTP"
|
|
197
|
-
helo = session.helo.to_s
|
|
216
|
+
helo = session.helo.to_s
|
|
217
|
+
helo = helo.empty? ? "unknown" : ascii_only_helo(session, helo)
|
|
198
218
|
"Received: from #{helo} (unknown [#{session.remote_ip}]) " \
|
|
199
219
|
"by #{session.helo_response} with #{protocol}; #{Time.now.utc.strftime("%a, %d %b %Y %H:%M:%S +0000")}\r\n"
|
|
200
220
|
end
|
|
@@ -233,6 +253,10 @@ module MailSmtp
|
|
|
233
253
|
def helo(session, name)
|
|
234
254
|
end
|
|
235
255
|
|
|
256
|
+
# Verify +username+/+password+ and raise +AuthenticationFailed+ if they do
|
|
257
|
+
# not check out. +authorization_id+ is the SASL PLAIN authzid — an
|
|
258
|
+
# unverified claim by the client, safely ignored by default. To honor it,
|
|
259
|
+
# verify the account may act as it and return that identity.
|
|
236
260
|
def authenticate(session, username:, password:, authorization_id: "")
|
|
237
261
|
raise AuthenticationFailed
|
|
238
262
|
end
|
|
@@ -611,8 +635,12 @@ module MailSmtp
|
|
|
611
635
|
respond(io, session, reply(221, "Service closing transmission channel")) unless session.close_after_reply
|
|
612
636
|
rescue StopConnection, StopService
|
|
613
637
|
raise
|
|
614
|
-
|
|
615
|
-
|
|
638
|
+
# A hang-up (FIN, or an RST from closing without draining our reply) is
|
|
639
|
+
# routine, not an error — skip the 421. Must precede the
|
|
640
|
+
# ServiceUnavailable rescue below, which ConnectionLost extends.
|
|
641
|
+
# ETIMEDOUT is excluded: retransmit exhaustion is a real network fault.
|
|
642
|
+
rescue EOFError, ConnectionLost, Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED => e
|
|
643
|
+
log(session, Logger::DEBUG, "Connection lost due abort by client! (#{e.class})", err: e)
|
|
616
644
|
rescue ServiceUnavailable => e
|
|
617
645
|
session.exceptions += 1
|
|
618
646
|
log(session, Logger::ERROR, loggable_error(e), err: e)
|
|
@@ -640,6 +668,38 @@ module MailSmtp
|
|
|
640
668
|
@connections_mutex.synchronize { @connection_ips.values.count(remote_ip) }
|
|
641
669
|
end
|
|
642
670
|
|
|
671
|
+
# RFC 5321 §4.1.1.1 constrains a HELO name, but refusing junk outright
|
|
672
|
+
# rejects too much mail — replace what's unsafe instead. CR/LF are already
|
|
673
|
+
# stripped (stops header injection); this stops header *forgery*, where an
|
|
674
|
+
# unbalanced "(" comments out the rest of a generated Received line for
|
|
675
|
+
# any consumer that parses those comments as data. Substitution is 1:1 so
|
|
676
|
+
# distinct names never collapse together.
|
|
677
|
+
def neuter_helo(name)
|
|
678
|
+
# Non-ASCII survives only when the server offers SMTPUTF8 (server-wide,
|
|
679
|
+
# since HELO precedes MAIL FROM) and the bytes are valid UTF-8;
|
|
680
|
+
# otherwise fall back to the ASCII-only rule.
|
|
681
|
+
if @smtputf8
|
|
682
|
+
utf8 = name.dup.force_encoding(Encoding::UTF_8)
|
|
683
|
+
return utf8.gsub(HELO_UNSAFE_UTF8, "?") if utf8.valid_encoding?
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
name.gsub(HELO_UNSAFE, "?")
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
# Header bodies are US-ASCII (RFC 5322 §2.2) unless this particular
|
|
690
|
+
# message negotiated SMTPUTF8 (RFC 6532) — known only here, with the
|
|
691
|
+
# envelope in hand. Re-checks the encoding rather than trusting
|
|
692
|
+
# +session.helo+ to still be what +neuter_helo+ produced, since it's a
|
|
693
|
+
# public accessor and +received_header+ is an override point.
|
|
694
|
+
def ascii_only_helo(session, helo)
|
|
695
|
+
if session.envelope.encoding_utf8
|
|
696
|
+
utf8 = helo.dup.force_encoding(Encoding::UTF_8)
|
|
697
|
+
return utf8 if utf8.valid_encoding?
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
helo.b.gsub(HELO_UNSAFE, "?")
|
|
701
|
+
end
|
|
702
|
+
|
|
643
703
|
# Message lines lose only their terminator. Bare LF is rejected when
|
|
644
704
|
# +forbid_bare_newline+ is set (default); see +read_data_line!+.
|
|
645
705
|
def strip_line_terminator(line)
|
|
@@ -652,8 +712,12 @@ module MailSmtp
|
|
|
652
712
|
def loggable_command(session, line)
|
|
653
713
|
if auth_value_phase?(session.phase)
|
|
654
714
|
"[redacted credential]"
|
|
655
|
-
|
|
715
|
+
elsif line.match?(/\AAUTH\s/i)
|
|
656
716
|
line.sub(/\A(AUTH\s+\S+)\s+\S.*\z/i, '\1 [redacted]')
|
|
717
|
+
else
|
|
718
|
+
# Every command runs through here whether or not DEBUG is enabled, so
|
|
719
|
+
# the traffic that carries no credential skips the rewrite entirely.
|
|
720
|
+
line
|
|
657
721
|
end
|
|
658
722
|
end
|
|
659
723
|
|
|
@@ -688,6 +752,10 @@ module MailSmtp
|
|
|
688
752
|
end
|
|
689
753
|
rescue Timeout::Error, IO::TimeoutError
|
|
690
754
|
raise ServiceUnavailable, "Write timed out"
|
|
755
|
+
rescue Errno::EPIPE, Errno::ECONNRESET, Errno::ECONNABORTED
|
|
756
|
+
# The peer went away mid-reply. Nothing further can be delivered, so do
|
|
757
|
+
# not let this surface as a server-side error and provoke a second write.
|
|
758
|
+
raise ConnectionLost, "Connection lost while writing"
|
|
691
759
|
end
|
|
692
760
|
|
|
693
761
|
# Success and informational replies. Pass +enhanced: nil+ for replies that
|
|
@@ -701,6 +769,14 @@ module MailSmtp
|
|
|
701
769
|
end
|
|
702
770
|
|
|
703
771
|
def process_command_line(session, line)
|
|
772
|
+
# An allowlisted peer skips the per-IP cap until PROXY names the real
|
|
773
|
+
# client, but nothing forces PROXY to actually arrive — require it
|
|
774
|
+
# before anything else (PROXY v1: a receiver must not guess whether the
|
|
775
|
+
# header is there), rather than leaving an indefinite exemption.
|
|
776
|
+
if proxy_allowed?(session.remote_ip) && !session.proxy && !line.match?(/\APROXY(\s+)/i)
|
|
777
|
+
raise ServiceUnavailable, "Abort connection while PROXY required from #{session.remote_ip}"
|
|
778
|
+
end
|
|
779
|
+
|
|
704
780
|
case line
|
|
705
781
|
when /\A(HELO|EHLO)(\s+.*)?\z/i
|
|
706
782
|
process_helo(session, line)
|
|
@@ -748,6 +824,7 @@ module MailSmtp
|
|
|
748
824
|
match = line.match(/\A(HELO|EHLO)(?:\s+(.*))?\z/i)
|
|
749
825
|
name = match[2].to_s.strip
|
|
750
826
|
raise InvalidParameters if name.empty?
|
|
827
|
+
name = neuter_helo(name)
|
|
751
828
|
|
|
752
829
|
# RFC 5321 §4.1.4: a later EHLO/HELO resets state exactly as RSET.
|
|
753
830
|
session.reset if session.phase != :helo
|
|
@@ -892,7 +969,9 @@ module MailSmtp
|
|
|
892
969
|
# refused with 555 (RFC 1869 §6.1) — never silently dropped.
|
|
893
970
|
def apply_mail_from_extensions!(session, address)
|
|
894
971
|
if (declared = address[/\sSIZE=(\d+)\b/i, 1])
|
|
895
|
-
|
|
972
|
+
# Base 10 explicitly: Integer("0100") would otherwise read as octal and
|
|
973
|
+
# under-report the declared size, and Integer("08") would raise.
|
|
974
|
+
declared = Integer(declared, 10)
|
|
896
975
|
session.envelope.declared_size = declared
|
|
897
976
|
if @max_message_size && declared > @max_message_size
|
|
898
977
|
raise MessageTooLarge, "Declared #{declared} bytes, limit is #{@max_message_size}"
|
|
@@ -966,10 +1045,17 @@ module MailSmtp
|
|
|
966
1045
|
reader = PrefixedReader.new(received_header(session), reader) if @add_received
|
|
967
1046
|
|
|
968
1047
|
error = nil
|
|
1048
|
+
aborting = false
|
|
969
1049
|
begin
|
|
970
1050
|
receiving_started(session)
|
|
971
1051
|
receive(session, reader)
|
|
1052
|
+
rescue ConnectionLost
|
|
1053
|
+
# Nothing left to drain toward, and no reply to line up behind the body.
|
|
1054
|
+
aborting = true
|
|
1055
|
+
raise
|
|
972
1056
|
rescue ServiceUnavailable
|
|
1057
|
+
# The app is refusing with a 421 — keep draining to the terminating "."
|
|
1058
|
+
# so a sender still writing the body isn't hit with a write-side reset.
|
|
973
1059
|
raise
|
|
974
1060
|
rescue Error => e
|
|
975
1061
|
error = e
|
|
@@ -977,7 +1063,7 @@ module MailSmtp
|
|
|
977
1063
|
log(session, Logger::ERROR, loggable_error(e), err: e)
|
|
978
1064
|
error = LocalError.new
|
|
979
1065
|
ensure
|
|
980
|
-
reader.drain
|
|
1066
|
+
reader.drain unless aborting
|
|
981
1067
|
session.reset
|
|
982
1068
|
# RFC 5321 §4.5.3.1.7: after 552 for SIZE, the server may close.
|
|
983
1069
|
# Same when drain hit the absolute ceiling with no "." in sight.
|
|
@@ -1022,7 +1108,7 @@ module MailSmtp
|
|
|
1022
1108
|
IO.select(nil, [ io.to_io ], nil, IO_WAIT_TIMEOUT)
|
|
1023
1109
|
end
|
|
1024
1110
|
|
|
1025
|
-
raise
|
|
1111
|
+
raise ConnectionLost, "Connection lost during DATA" if io.closed?
|
|
1026
1112
|
raise ServiceUnavailable, "Server shutting down" if shutdown?
|
|
1027
1113
|
next
|
|
1028
1114
|
end
|
|
@@ -1043,7 +1129,7 @@ module MailSmtp
|
|
|
1043
1129
|
rescue BufferOverrun
|
|
1044
1130
|
raise ServiceUnavailable, "Input buffer overrun"
|
|
1045
1131
|
rescue EOFError, Errno::ECONNRESET, Errno::EPIPE, Errno::ECONNABORTED
|
|
1046
|
-
raise
|
|
1132
|
+
raise ConnectionLost, "Connection lost during DATA"
|
|
1047
1133
|
end
|
|
1048
1134
|
|
|
1049
1135
|
def monotonic_time
|
|
@@ -1109,14 +1195,48 @@ module MailSmtp
|
|
|
1109
1195
|
raise InvalidParameters
|
|
1110
1196
|
end
|
|
1111
1197
|
|
|
1112
|
-
|
|
1113
|
-
|
|
1198
|
+
# The SASL PLAIN authzid is a claim by the client, not a fact (RFC 4954
|
|
1199
|
+
# §4): "victim\0attacker\0password" asks us to treat a login as +victim+.
|
|
1200
|
+
# The application grants it by returning that identity from
|
|
1201
|
+
# +authenticate+; any other return (nil, true, a record) falls back to
|
|
1202
|
+
# the account that proved the password. The raw claim stays available as
|
|
1203
|
+
# +session.requested_authorization_id+.
|
|
1204
|
+
def authorize(session, requested_authorization_id, authentication_id, authentication)
|
|
1205
|
+
# base64 can carry bytes the command grammar never allows through;
|
|
1206
|
+
# CR/LF here would forge header lines when an application logs who
|
|
1207
|
+
# authenticated.
|
|
1208
|
+
if [ requested_authorization_id, authentication_id ].any? { |value| value.to_s.match?(/[\r\n]/) }
|
|
1209
|
+
raise InvalidParameters
|
|
1210
|
+
end
|
|
1211
|
+
|
|
1212
|
+
granted = authenticate(session,
|
|
1114
1213
|
username: authentication_id,
|
|
1115
1214
|
password: authentication,
|
|
1116
|
-
authorization_id:
|
|
1117
|
-
|
|
1215
|
+
authorization_id: requested_authorization_id)
|
|
1216
|
+
# A non-empty String or Symbol is the application naming the identity —
|
|
1217
|
+
# authoritative, and may differ from the client's claim (canonicalized
|
|
1218
|
+
# case, an alias resolved to its primary address).
|
|
1219
|
+
granted = granted.to_s if granted.is_a?(String) || granted.is_a?(Symbol)
|
|
1220
|
+
granted_explicitly = granted.is_a?(String) && !granted.empty?
|
|
1221
|
+
# The grant gets the same CR/LF check as the claim: an
|
|
1222
|
+
# application-assembled identity can carry it just as easily.
|
|
1223
|
+
raise LocalError if granted_explicitly && granted.match?(/[\r\n]/)
|
|
1224
|
+
|
|
1225
|
+
authorization_id = granted_explicitly ? granted : authentication_id
|
|
1226
|
+
|
|
1227
|
+
# Recorded even when the login is about to be refused, so an application
|
|
1228
|
+
# can audit who was impersonated.
|
|
1229
|
+
session.requested_authorization_id = requested_authorization_id
|
|
1230
|
+
|
|
1231
|
+
# Refuse only when the client claimed a different identity and the
|
|
1232
|
+
# application didn't grant one — asking to be yourself is allowed (RFC
|
|
1233
|
+
# 4616 §2). Compared as bytes: both sides came out of base64 as BINARY.
|
|
1234
|
+
requested = requested_authorization_id.to_s
|
|
1235
|
+
if !requested.empty? && !granted_explicitly && requested.b != authentication_id.to_s.b
|
|
1236
|
+
raise AuthenticationFailed
|
|
1237
|
+
end
|
|
1118
1238
|
|
|
1119
|
-
session.authorization_id = authorization_id
|
|
1239
|
+
session.authorization_id = authorization_id
|
|
1120
1240
|
session.authentication_id = authentication_id
|
|
1121
1241
|
session.authenticated_at = Time.now.utc
|
|
1122
1242
|
reply(235, "OK", enhanced: "2.7.0")
|
|
@@ -1189,9 +1309,12 @@ module MailSmtp
|
|
|
1189
1309
|
# already gone
|
|
1190
1310
|
end
|
|
1191
1311
|
|
|
1192
|
-
#
|
|
1193
|
-
#
|
|
1194
|
-
# the
|
|
1312
|
+
# Body lines are not covered here: once DATA is accepted, +read_data_line!+
|
|
1313
|
+
# consumes the buffer directly and never consults this. Sending the DATA
|
|
1314
|
+
# verb itself with the body already behind it is still refused, because the
|
|
1315
|
+
# client is required to wait for the 354. PROXY may be followed immediately
|
|
1316
|
+
# by HELO/EHLO before the extension finishes negotiating the real client IP
|
|
1317
|
+
# (typical load-balancer / PROXY clients).
|
|
1195
1318
|
def pipelining_allowed?(session)
|
|
1196
1319
|
@pipelining || (proxy_enabled? && session.phase == :helo)
|
|
1197
1320
|
end
|
data/lib/mailsmtp/session.rb
CHANGED
|
@@ -19,7 +19,8 @@ module MailSmtp
|
|
|
19
19
|
attr_accessor :phase,
|
|
20
20
|
:local_ip, :remote_ip, :helo, :helo_response, :local_response,
|
|
21
21
|
:connected_at, :encrypted_at, :authenticated_at,
|
|
22
|
-
:authorization_id, :authentication_id, :
|
|
22
|
+
:authorization_id, :authentication_id, :requested_authorization_id,
|
|
23
|
+
:auth_login_username,
|
|
23
24
|
:exceptions, :auth_failures, :proxy, :close_after_reply,
|
|
24
25
|
:tls_version, :tls_cipher
|
|
25
26
|
attr_reader :envelope
|
|
@@ -49,6 +50,7 @@ module MailSmtp
|
|
|
49
50
|
@authenticated_at = nil
|
|
50
51
|
@authorization_id = nil
|
|
51
52
|
@authentication_id = nil
|
|
53
|
+
@requested_authorization_id = nil
|
|
52
54
|
@auth_login_username = nil
|
|
53
55
|
end
|
|
54
56
|
|
data/lib/mailsmtp/version.rb
CHANGED
data/mailsmtp.gemspec
CHANGED
|
@@ -8,7 +8,7 @@ Gem::Specification.new do |spec|
|
|
|
8
8
|
spec.authors = [ "Simon Lev" ]
|
|
9
9
|
|
|
10
10
|
spec.summary = "Boring SMTP server for Ruby."
|
|
11
|
-
spec.description = "Answers one question: how do I receive emails in Ruby? Implements the SMTP protocol so you can plug in your own logic for auth, accepting mail, and storage."
|
|
11
|
+
spec.description = "Answers one question: how do I receive emails in Ruby? Implements the SMTP protocol so you can plug in your own logic for auth, accepting mail, and storage. An SMTP server library for building an MX or submission listener."
|
|
12
12
|
|
|
13
13
|
spec.homepage = "https://github.com/mailpiece/mailsmtp"
|
|
14
14
|
spec.license = "MIT"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: mailsmtp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Simon Lev
|
|
@@ -38,7 +38,8 @@ dependencies:
|
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
39
|
version: '0'
|
|
40
40
|
description: 'Answers one question: how do I receive emails in Ruby? Implements the
|
|
41
|
-
SMTP protocol so you can plug in your own logic for auth, accepting mail, and storage.
|
|
41
|
+
SMTP protocol so you can plug in your own logic for auth, accepting mail, and storage.
|
|
42
|
+
An SMTP server library for building an MX or submission listener.'
|
|
42
43
|
executables: []
|
|
43
44
|
extensions: []
|
|
44
45
|
extra_rdoc_files: []
|