lmtp 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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: []