lmtp 0.0.1

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.
Files changed (5) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +25 -0
  3. data/README.rdoc +97 -0
  4. data/lib/lmtp.rb +380 -0
  5. metadata +55 -0
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 152778817318a42ad2aec86517d416b945e51fae
4
+ data.tar.gz: 4bc49d66edc5055c69681bc4d1079b62dababd9f
5
+ SHA512:
6
+ metadata.gz: 40fee91812ede656b71e820d3b74fff8a013e2068ad72f130a2c51b06daf27ffa680c20212ab1b63b830f2a7483bab1ea5688be6f1ad82ce4bb86ac1d098d4c1
7
+ data.tar.gz: 6bf889d743d0981615b7769a4184c1eea452d34c246d9c394b99e1c7663027c58327c47c973da9b05770c1ed0941f0852f7efedc81ee49a72d11ca82b7f9650d
data/LICENSE ADDED
@@ -0,0 +1,25 @@
1
+ Copyright © 2015 Marvin Gülker
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without
5
+ modification, are permitted provided that the following conditions are
6
+ met:
7
+
8
+ 1. Redistributions of source code must retain the above copyright
9
+ notice, this list of conditions and the following disclaimer.
10
+
11
+ 2. Redistributions in binary form must reproduce the above copyright
12
+ notice, this list of conditions and the following disclaimer in the
13
+ documentation and/or other materials provided with the distribution.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
+ HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,97 @@
1
+ = Minimal LMTP Server for Ruby
2
+
3
+ This project is a minimal LMTP server for Ruby. Ever wanted to stick
4
+ your Ruby application behind a real MTA like Postfix so it can receive
5
+ mails directly? This library is what you need.
6
+
7
+ This library provides a single class, LmtpServer, which will open a
8
+ UNIX domain socket and listen for LMTP commands on it. You then direct
9
+ your MTA to deliver the mails it receives via LMTP over the socket
10
+ opened by this library. The email (and surroundings) will then show up
11
+ in form of callbacks in your application. To speak in terms of email
12
+ administration, this library thus implements an MDA (mail delivery
13
+ agent).
14
+
15
+ It does not provide a means to actually process the emails you
16
+ receive, you will receive them as a large string with all the original
17
+ CRLF line separators included. It is recommended to use a library like
18
+ mail[https://github.com/mikel/mail] for that.
19
+
20
+ == Dependencies
21
+
22
+ None but Ruby’s standard +socket+ library.
23
+
24
+ == Usage
25
+
26
+ require "lmtp"
27
+
28
+ LmtpServer.new("/var/spool/postfix/private/ruby-lmtp") do |msg|
29
+ puts "-- Start email --"
30
+ puts msg
31
+ puts "-- End email --"
32
+ end
33
+
34
+ You can find a lot more examples in the documentation of the
35
+ LmtpServer class.
36
+
37
+ == Security considerations
38
+
39
+ LMTP (RFC 2033) was not designed to be used over the WWW and the RFC
40
+ actively recommends against such usage. LMTP contains no
41
+ authentication and similar security measures; it is only intended to
42
+ be used in a local, trusted environment, which is the reason why this
43
+ library only provides UNIX domain sockets for listening and not TCP
44
+ sockets.
45
+
46
+ Also keep in mind that this library is fairly minimal. While I think I
47
+ have implemented RFC 2033 correctly, I have not intensly read RFC 821
48
+ (SMTP), which is referenced from the LMTP specification at certain
49
+ parts. If unusual commands are used over the UNIX socket, unexpected
50
+ things may happen in form of exceptions. Feel free to report such
51
+ behaviour as a bug.
52
+
53
+ The library is not multithreaded. You can threadsafely call
54
+ LmtpServer#stop, but email messages will be proceeded one by one. If
55
+ multiple clients connect to the UNIX domain socket, they will be
56
+ queued and handled after one another. That is, if one client does not
57
+ quit the connection, the entire server is blocked until the
58
+ (configurable) timeout value is reached.
59
+
60
+ == Links
61
+
62
+ * Repository: https://github.com/Quintus/ruby-lmtp
63
+ * Bugtracker: https://github.com/Quintus/ruby-lmtp/issues
64
+ * The original Gist from which this project is a continuation:
65
+ https://gist.github.com/Quintus/f4faf26aa022f74f7778
66
+
67
+ While you are at it, check out my (German)
68
+ Blog[http://www.quintilianus.eu].
69
+
70
+ == License
71
+
72
+ Copyright © 2015 Marvin Gülker
73
+
74
+ All rights reserved.
75
+
76
+ Redistribution and use in source and binary forms, with or without
77
+ modification, are permitted provided that the following conditions are
78
+ met:
79
+
80
+ 1. Redistributions of source code must retain the above copyright
81
+ notice, this list of conditions and the following disclaimer.
82
+
83
+ 2. Redistributions in binary form must reproduce the above copyright
84
+ notice, this list of conditions and the following disclaimer in the
85
+ documentation and/or other materials provided with the distribution.
86
+
87
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
88
+ "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
89
+ LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
90
+ A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
91
+ HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
92
+ SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
93
+ LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
94
+ DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
95
+ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
96
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
97
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
@@ -0,0 +1,380 @@
1
+ # -*- coding: utf-8 -*-
2
+ # Copyright © 2015 Marvin Gülker
3
+ # All rights reserved.
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are
7
+ # met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright
10
+ # notice, this list of conditions and the following disclaimer.
11
+ #
12
+ # 2. Redistributions in binary form must reproduce the above copyright
13
+ # notice, this list of conditions and the following disclaimer in the
14
+ # documentation and/or other materials provided with the distribution.
15
+ #
16
+ # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
+ # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
+ # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
+ # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
+ # HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
+ # SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
+ # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
+ # DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
+ # THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
+ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
+ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
+
28
+ require "socket"
29
+
30
+ # LMTP server class. Instances of this class utilize a UNIX socket to implement
31
+ # the LMTP protocol (see {RFC 2033}[https://tools.ietf.org/html/rfc1854]) in its
32
+ # most minimal and basic form. LMTP is spoken by possibly any MTA, so using this
33
+ # class you can make your Ruby program an email endpoint as long as you know how
34
+ # to configure your MTA. I’ve only tested with Postfix, though, so no guarantees.
35
+ #
36
+ # Instances of this class support several callbacks. The main callback is the message
37
+ # callback, which is passed the the block to ::new. It gets called whenever the
38
+ # LMTP client hands in an email, and receives the entire email as plain text
39
+ # as its argument. You can use the “mail” library or other means to parse it.
40
+ # Other callbacks you might find useful can be set with the #logging and #headers
41
+ # methods.
42
+ #
43
+ # This class makes no use of threads for multiple connections. Thus,
44
+ # any emails submitted at once to the UNIX domain socket are processed
45
+ # ony-by-one. A single client could block all other clients thus, but
46
+ # because LMTP should only ever be used in a completely trusted
47
+ # environment (see RFC 2033, sections 3 and 5), this is not an issue.
48
+ #
49
+ # It _does_ employ a mutex for the #stop method and the checking of the
50
+ # stopping variable. This means you can safely call #stop from another
51
+ # thread.
52
+ #
53
+ # Example use:
54
+ #
55
+ # server = LmtpServer.new("/var/spool/postfix/private/mysocket") do |message|
56
+ # puts "--- Start of email ---"
57
+ # puts message
58
+ # puts "--- End of email ---"
59
+ # end
60
+ #
61
+ # server.logging do |level, msg|
62
+ # $stderr.puts "[#{level}] #{msg}"
63
+ # end
64
+ #
65
+ # server.start
66
+ #
67
+ # The LMTP server by this class implements the following SMTP Service
68
+ # Extensions (see below for the list of RFCs). Do not implement them
69
+ # yourself by utilising #moreextensions and the callbacks, they’re
70
+ # there already!
71
+ #
72
+ # * PIPELINING
73
+ # * ENHANCEDSTATUSCODES
74
+ # * 8BITMIME
75
+ #
76
+ # RFCs implemented by this class:
77
+ #
78
+ # * {RFC 2033}[https://tools.ietf.org/html/rfc2033]
79
+ # * {RFC 2034}[https://tools.ietf.org/html/rfc2034]
80
+ # * {RFC 1854}[https://tools.ietf.org/html/rfc1854]
81
+ # * {RFC 1869}[https://tools.ietf.org/html/rfc1869]
82
+ # * {RFC 1652}[https://tools.ietf.org/html/rfc1652]
83
+ # * {RFC 821}[https://tools.ietf.org/html/rfc821] for the minimally required parts
84
+ class LmtpServer
85
+
86
+ # The machine’s hostname is read from this file.
87
+ HOSTNAME_FILE = "/etc/hostname".freeze
88
+
89
+ # Version of this library.
90
+ VERSION = "0.0.1".freeze
91
+
92
+ # Timeout in seconds when a client is forcibly disconnected when
93
+ # it does nothing.
94
+ attr_accessor :timeout
95
+
96
+ # This is an array of extra extensions that are announced to
97
+ # the client in response to LHLO. Just append the names of
98
+ # the extensions to this array (e.g. "MYCOOLEXTENSION").
99
+ # The class will take care to prefix is with the proper LMTP
100
+ # reply code.
101
+ #
102
+ # This array is empty by default. Modifying it only makes sense
103
+ # if you actually implement the extensions you advertise here.
104
+ attr_accessor :moreextensions
105
+
106
+ # Message text to return on a successful message acceptance. This is
107
+ # automatically prefixed by "250 2.6.0 " so you don’t have to care
108
+ # about the LMTP status code. This text is purely informational and
109
+ # has no meaning to the protocol. It will show up in the sending
110
+ # MTA’s logs.
111
+ attr_accessor :successmsg
112
+
113
+ # Create a new LMTP server.
114
+ #
115
+ # === Parameters
116
+ # [path]
117
+ # Path on which the UNIX domain socket is created.
118
+ # All parent directories must exist, but the “file” itself
119
+ # must not exist (an ArgumentError is thrown if it exists).
120
+ # [mode (nil)]
121
+ # UNIX permissions to set on the UNIX socket file as
122
+ # a numeric mode (example: 0666 for rw-rw-rw-).
123
+ # User and Group of the file are determined by whatever
124
+ # the process environment mandates. +nil+ means to use
125
+ # whatever the process umask mandates.
126
+ # [callback]
127
+ # Message callback. Receives any email as a string that is
128
+ # passed to this LMTP server. The string will contain the
129
+ # original carriagereturn+newline line breaks from the protcol.
130
+ #
131
+ # === Return value
132
+ # Returns the new instance.
133
+ def initialize(path, mode = nil, &callback)
134
+ @path = path
135
+ @mode = mode
136
+ @hostname = File.read(HOSTNAME_FILE).strip
137
+ @msgcb = callback
138
+ @client = nil
139
+ @timeout = 30
140
+ @mutex = Mutex.new
141
+ @do_stop = false
142
+ @headercb = method(:default_headercb)
143
+ @moreextensions = []
144
+ @successmsg = "All your bytes are belong to us."
145
+
146
+ if File.exist?(path)
147
+ raise(ArgumentError, "File already exists: #{path}")
148
+ end
149
+ end
150
+
151
+ # Specify the logging callback. It will receive a syslog
152
+ # logging level as a symbol and the log message.
153
+ #
154
+ # By default, no logging callback is set and hence nothing
155
+ # is logged.
156
+ def logging(&callback)
157
+ @logcb = callback
158
+ end
159
+
160
+ # Override the callback used for responding to the LMTP client for
161
+ # the LMTP commands before DATA, e.g. MAIL FROM and RCPT TO. The
162
+ # callback receives the entire line the client sent, including the
163
+ # trailing carriagereturn-newline.
164
+ #
165
+ # The default callback only answers "250 2.1.0 ok" for every
166
+ # command. Note that RFC 2033 (LMTP) requires in section 5 that any
167
+ # LMTP server MUST implement RFC 2034, which in turn refers to RFC
168
+ # 1893 for the actual status codes, so for any replies you make you
169
+ # must make use of the extended statuscodes defined in RFC 1893 in
170
+ # the format defined by RFC 2034. Don’t worry — both of these RFCs
171
+ # are simple enough to just read quickly through them.
172
+ #
173
+ # Example:
174
+ #
175
+ # server.headers do |line|
176
+ # case line
177
+ # when /^MAIL FROM/ then "250 ok"
178
+ # when /^RCPT TO:<.*?>/ then
179
+ # if this_account_exists($1)
180
+ # "250 2.1.5 Recipient ok"
181
+ # else
182
+ # "550 5.1.1 Recipient does not exist over here."
183
+ # end
184
+ # else
185
+ # "250 2.1.0 ok"
186
+ # end
187
+ # end
188
+ #
189
+ # Note that the replies you define here are not immediately sent
190
+ # to the client, which is a result of the PIPELINING extension
191
+ # that is required by LMTP (see RFC 2033, section 5, and RFC 1854).
192
+ # Instead they’re accumulated and send as a big swall to the client
193
+ # when he issues the DATA command.
194
+ def headers(&callback)
195
+ @headercb = callback
196
+ end
197
+
198
+ # Halt the running server. This method is threadsafe.
199
+ def stop
200
+ @mutex.synchronize{ @do_stop = true }
201
+ end
202
+
203
+ # Create the UNIX domain socket and start listening on it.
204
+ # This method starts a listening loop and thus blocks.
205
+ # Use #stop from another thread to issue a halt.
206
+ def start
207
+ log :info, "Starting server"
208
+ @mutex.synchronize{ @do_stop = false }
209
+
210
+ UNIXServer.open(@path) do |server|
211
+ File.chmod(@mode, @path) if @mode
212
+
213
+ log :info, "Accepting connections."
214
+ while @client = server.accept
215
+ addr = @client.addr.last
216
+ log :info, "Client connect from #{addr}."
217
+
218
+ # TODO: Use #accept_nonblock in loop? This way, a client has to connect
219
+ # first to have the server shut down.
220
+ break if @mutex.synchronize{ @do_stop }
221
+
222
+ begin
223
+ catch :timeout do
224
+ handle_client
225
+ end
226
+ rescue => e
227
+ log :err, "Exception: #{e.class.name}: #{e.message}: #{e.backtrace.join("\n")}"
228
+ log :err, "Aborting connection due to exception."
229
+ @client.close
230
+ end
231
+
232
+ log :info, "Client connection closed: #{addr}"
233
+ @client = nil
234
+ end
235
+
236
+ end
237
+
238
+ log :info, "Server stopped."
239
+ ensure
240
+ if File.exist?(@path)
241
+ log :info, "Removing UNIX socket '#@path'"
242
+ File.delete(@path)
243
+ end
244
+ end
245
+
246
+ private
247
+
248
+ def log(level, msg)
249
+ @logcb.call(level, msg) if @logcb
250
+ end
251
+
252
+ def reply(msg)
253
+ str = msg.strip + "\r\n"
254
+ log :debug, "server: #{str.inspect}"
255
+ @client.puts(str)
256
+ end
257
+
258
+ def gets(raw = false)
259
+ if IO.select([@client], nil, nil, @timeout)
260
+ str = @client.gets
261
+ log :debug, "client: #{str.inspect}"
262
+ return nil if str.nil?
263
+
264
+ str.gsub!("\r\n", "\n") unless raw
265
+
266
+ if str.strip == "RSET" && !raw # Ensure that in DATA we can ignore it if this text occurs
267
+ reply "220 2.0.0 Resetting."
268
+ throw :rset
269
+ end
270
+
271
+ str
272
+ else
273
+ log :err, "Client #{@client.addr.last} timed out. Closing."
274
+ reply "422 4.5.0 Timeout."
275
+ @client.close
276
+ throw :timeout
277
+ end
278
+ end
279
+
280
+ def handle_client
281
+ reply "220 #{@hostname} LMTP server ready"
282
+
283
+ line = gets
284
+ if line !~ /^LHLO (.*?)$/
285
+ reply "500 5.5.1 You must great me first."
286
+ @client.close
287
+ return
288
+ end
289
+
290
+ log :info, "Client reports name: '#$1'"
291
+
292
+ reply "250-#{@hostname}"
293
+ reply "250-PIPELINING"
294
+ reply "250-ENHANCEDSTATUSCODES"
295
+ @moreextensions.each{|ext| reply("250-#{ext}")}
296
+ reply "250 8BITMIME"
297
+
298
+ loop do
299
+ no_valid_recipients = true
300
+
301
+ catch :rset do
302
+ # Allow pipelining by accumulation
303
+ responses = []
304
+ loop do
305
+ line = gets
306
+ break if line =~ /^DATA$/
307
+ response = @headercb.call(line)
308
+
309
+ # Conform to section 4.2(2) of RFC 2033. We need at least one RCPT
310
+ # command to succeed, otherwise DATA further below must fail.
311
+ if line.start_with?("RCPT") && response.start_with?("2")
312
+ no_valid_recipients = false
313
+ end
314
+
315
+ responses << response
316
+ end
317
+
318
+ # Answer the pipeline
319
+ responses.each do |response|
320
+ reply response
321
+ end
322
+
323
+ # Prepare for receiving
324
+ reply "354 Start data. End with <CRLF>.<CRLF>"
325
+ message = ""
326
+ loop do
327
+ line = gets(true) # Keep carriage returns and prevent RSET
328
+ break if line.strip == "."
329
+
330
+ # Honour transparency process as per section 4.5.2 of RFC 821
331
+ line.slice!(0) if line.start_with?(".") && line.strip.length > 1
332
+
333
+ message << line
334
+ end
335
+
336
+ # Conform to section 4.2(2) of RFC 2033 by failing with 503 if no valid
337
+ # recipients were found.
338
+ if no_valid_recipients
339
+ log :info, "No valid RCPT commands received, denying relay."
340
+ reply "503 5.0.0 No valid RCPT command received, denying DATA."
341
+ next
342
+ end
343
+
344
+ begin
345
+ log :debug, "Invoking message callback."
346
+ @msgcb.call(message)
347
+ rescue => e
348
+ reply "551 Internal error: #{e.class}: #{e.message}"
349
+ @client.close
350
+ return
351
+ end
352
+
353
+ reply "250 2.6.0 #@successmsg"
354
+
355
+ final = gets
356
+ if final
357
+ if final =~ /^QUIT$/
358
+ # Regular QUIT
359
+
360
+ reply "221 2.0.0 #{@hostname} Goodbye."
361
+ @client.close
362
+ return
363
+ else
364
+ # Not closing connection, client wants to sent another email
365
+ end
366
+ else
367
+ # Whoops. Client closed connection without QUIT. Bad guy!
368
+ log :warning, "Client closed connection without QUIT."
369
+ @client.close
370
+ return
371
+ end
372
+ end
373
+ end
374
+ end
375
+
376
+ def default_headercb(line)
377
+ "250 2.1.0 ok"
378
+ end
379
+
380
+ end
metadata ADDED
@@ -0,0 +1,55 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lmtp
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Marvin Gülker
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-05-26 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |
14
+ This library allows your application to act as an LMTP endpoint
15
+ so you can have MTAs like Postfix relay mail directly to your
16
+ application.
17
+ email: quintus@quintilianus.eu
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files:
21
+ - README.rdoc
22
+ - LICENSE
23
+ files:
24
+ - LICENSE
25
+ - README.rdoc
26
+ - lib/lmtp.rb
27
+ homepage: https://github.com/Quintus/ruby-lmtp
28
+ licenses:
29
+ - BSD
30
+ metadata: {}
31
+ post_install_message:
32
+ rdoc_options:
33
+ - "-t"
34
+ - LMTP library for Ruby
35
+ - "-m"
36
+ - README.rdoc
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: 2.0.0
44
+ required_rubygems_version: !ruby/object:Gem::Requirement
45
+ requirements:
46
+ - - ">="
47
+ - !ruby/object:Gem::Version
48
+ version: '0'
49
+ requirements: []
50
+ rubyforge_project:
51
+ rubygems_version: 2.4.5
52
+ signing_key:
53
+ specification_version: 4
54
+ summary: LMTP server library for Ruby
55
+ test_files: []