system_mail 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - 2.0.0
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Igor Bochkariov
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,55 @@
1
+ [![Build Status](https://travis-ci.org/ujifgc/system_mail.png)](https://travis-ci.org/ujifgc/system_mail)
2
+ [![Code Climate](https://codeclimate.com/github/ujifgc/system_mail.png)](https://codeclimate.com/github/ujifgc/system_mail)
3
+
4
+ ## Introduction
5
+
6
+ SystemMail is a Ruby library built to compose and deliver internet mail using
7
+ operating system utilities.
8
+
9
+ SystemMail features:
10
+
11
+ * tiny memory footprint even with big attachments
12
+ * blazing-fast gem loading and message composing
13
+ * alternating message body format: text, enriched, HTML
14
+ * rich capabilities in attaching files
15
+ * ability to combine HTML message with file attachments
16
+
17
+ Operating system commands used to do the job are:
18
+
19
+ * `sendmail -t < temp` or alternative sends the message to Mail Transfer Agent
20
+ * `base64 file >> temp` encodes binary files to textual form
21
+ * `file --mime-type --mime-encoding -b file` detects Content-Type and charset
22
+
23
+ ## Installation
24
+
25
+ Add this line to your application's Gemfile:
26
+
27
+ gem 'system_mail'
28
+
29
+ And then execute:
30
+
31
+ $ bundle
32
+
33
+ Or install it yourself as:
34
+
35
+ $ gem install system_mail
36
+
37
+ ## Usage
38
+
39
+ mail = SystemMail.new(
40
+ from: 'user@example.com',
41
+ to: ['user1@gmail.com', 'user2@gmail.com'],
42
+ subject: 'test проверочный subject',
43
+ files: ['Gemfile', 'Gemfile.lock'],
44
+ text: 'big small норм',
45
+ html: File.read('test.html')
46
+ )
47
+ mail.deliver
48
+
49
+ ## Contributing
50
+
51
+ 1. Fork it
52
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
53
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
54
+ 4. Push to the branch (`git push origin my-new-feature`)
55
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ task :default => :test
5
+
6
+ Rake::TestTask.new :test do |t|
7
+ t.libs << "test"
8
+ t.test_files = FileList['test/**/*_test.rb']
9
+ end
@@ -0,0 +1,251 @@
1
+ require 'base64'
2
+ require 'shellwords'
3
+ require 'system_mail/storage'
4
+
5
+ module SystemMail
6
+ ##
7
+ # Manages compiling an email message from various attributes and files.
8
+ #
9
+ class Message
10
+ BASE64_SIZE = 76
11
+ UTF8_SIZE = 998
12
+ SETTINGS = {
13
+ :sendmail => '/usr/sbin/sendmail -t',
14
+ :base64 => 'base64',
15
+ :file => 'file --mime-type --mime-encoding -b',
16
+ :storage => ENV['TMP'] || '/tmp',
17
+ }.freeze
18
+
19
+ ##
20
+ # Creates new message. Available options:
21
+ #
22
+ # - :text, String, Textual part of the message
23
+ # - :enriched, String, Enriched alternative of the message (RFC 1896)
24
+ # - :html, String, HTML alternative of the message
25
+ # - :from, String, 'From:' header for the message
26
+ # - :to, String or Array of Strings, 'To:' header, if Arrey, it gets joined by ', '
27
+ # - :subject, String, Subject of the message, it gets encoded automatically
28
+ # - :files, File or String of file path or Array of them, Attachments of the message
29
+ #
30
+ # Options :text, :enriched and :html are interchangeable.
31
+ # Option :to is required.
32
+ #
33
+ # Examples:
34
+ #
35
+ # mail = Message.new(
36
+ # :from => 'user@example.com',
37
+ # :to => 'user@gmail.com',
38
+ # :subject => 'test subject',
39
+ # :text => 'big small normal',
40
+ # :html => File.read('test.html'),
41
+ # :attachments => [File.open('Gemfile'), 'attachment.zip'],
42
+ # )
43
+ #
44
+ def initialize(options={})
45
+ @body = {}
46
+ @to = []
47
+ @mutex = Mutex.new
48
+ %W(text enriched html from to subject attachments).each do |option|
49
+ name = option.to_sym
50
+ send(name, options[name])
51
+ end
52
+ end
53
+
54
+ ##
55
+ # Delivers the message using sendmail.
56
+ #
57
+ # Example:
58
+ #
59
+ # mail.deliver #=> nil
60
+ #
61
+ def deliver
62
+ validate
63
+ with_storage do
64
+ write_headers
65
+ write_message
66
+ send_message
67
+ end
68
+ end
69
+
70
+ def settings
71
+ @settings ||= SETTINGS.dup
72
+ end
73
+
74
+ private
75
+
76
+ def text(input)
77
+ @body['text'] = input
78
+ end
79
+
80
+ def enriched(input)
81
+ @body['enriched'] = input
82
+ end
83
+
84
+ def html(input)
85
+ @body['html'] = input
86
+ end
87
+
88
+ def from(input)
89
+ @from = input
90
+ end
91
+
92
+ def to(input)
93
+ @to += Array(input)
94
+ end
95
+
96
+ def subject(input)
97
+ @subject = input
98
+ end
99
+
100
+ def attachments(input)
101
+ input && input.each{ |file| add_file(*file) }
102
+ end
103
+
104
+ def add_file(name, path = nil)
105
+ path ||= name
106
+ name = File.basename(name)
107
+ files[name] = path
108
+ end
109
+
110
+ def files
111
+ @files ||= {}
112
+ end
113
+
114
+ def with_storage
115
+ @mutex.synchronize do
116
+ @storage = Storage.new settings[:storage]
117
+ yield
118
+ @storage.clear
119
+ @storage = nil
120
+ end
121
+ end
122
+
123
+ def write_message
124
+ if files.any?
125
+ multipart :mixed do |boundary|
126
+ write_part boundary
127
+ write_body
128
+ files.each_pair do |name, path|
129
+ write_part boundary
130
+ write_file name, path
131
+ end
132
+ end
133
+ else
134
+ write_body
135
+ end
136
+ end
137
+
138
+ def write_body
139
+ case @body.length
140
+ when 0
141
+ nil
142
+ when 1
143
+ data, type = @body.first
144
+ write_content data, "text/#{type}"
145
+ else
146
+ multipart :alternative do |boundary|
147
+ %w[text enriched html].each do |type|
148
+ data = @body[type] || next
149
+ write_part boundary
150
+ write_content data, "text/#{type}"
151
+ end
152
+ end
153
+ end
154
+ end
155
+
156
+ def write_content(data, content_type)
157
+ if data.bytesize < UTF8_SIZE || !data.lines.any?{ |line| line.bytesize > UTF8_SIZE }
158
+ write_8bit_data data, content_type
159
+ else
160
+ write_base64_data data, content_type
161
+ end
162
+ end
163
+
164
+ def multipart(type)
165
+ boundary = new_boundary(type)
166
+ @storage.puts "Content-Type: multipart/#{type}; boundary=\"#{boundary}\""
167
+ yield boundary
168
+ write_part boundary, :end
169
+ end
170
+
171
+ def write_headers
172
+ @storage.puts "From: #{@from}" if @from
173
+ @storage.puts "To: #{@to.join(', ')}"
174
+ @storage.puts "Subject: #{encode_subject}"
175
+ end
176
+
177
+ def write_part(boundary, type = :begin)
178
+ @storage.puts
179
+ @storage.puts "--#{boundary}#{type == :end ? '--' : ''}"
180
+ end
181
+
182
+ def write_base64_data(data, content_type)
183
+ @storage.puts "Content-Type: #{content_type}; charset=#{data.encoding}"
184
+ @storage.puts "Content-Transfer-Encoding: base64"
185
+ @storage.puts
186
+ Base64.strict_encode64(data).each_slice(BASE64_SIZE) do |line|
187
+ @storage.puts line
188
+ end
189
+ end
190
+
191
+ def write_8bit_data(data, content_type)
192
+ @storage.puts "Content-Type: #{content_type}; charset=#{data.encoding}"
193
+ @storage.puts "Content-Transfer-Encoding: 8bit"
194
+ @storage.puts
195
+ @storage.puts data
196
+ end
197
+
198
+ def write_file(name, file)
199
+ path = case file
200
+ when String
201
+ fail Errno::ENOENT, file unless File.file?(file)
202
+ fail Errno::EACCES, file unless File.readable?(file)
203
+ file
204
+ when File
205
+ file.path
206
+ else
207
+ fail ArgumentError, 'attachment must be File or String of file path'
208
+ end
209
+ write_base64_file name, path
210
+ end
211
+
212
+ def write_base64_file(name, path)
213
+ @storage.puts "Content-Type: #{read_mime(path)}"
214
+ @storage.puts "Content-Transfer-Encoding: base64"
215
+ @storage.puts "Content-Disposition: attachment; filename=\"#{name}\""
216
+ @storage.puts
217
+ @storage.capture do |message_path|
218
+ `#{settings[:base64]} '#{path.shellescape}' >> #{message_path}`
219
+ end
220
+ end
221
+
222
+ def send_message
223
+ if @storage.file?
224
+ @storage.capture do |message_path|
225
+ `#{settings[:sendmail]} < #{message_path}`
226
+ end
227
+ else
228
+ IO.popen(settings[:sendmail]) do |io|
229
+ io.write(@storage.read)
230
+ end
231
+ end
232
+ end
233
+
234
+ def read_mime(file_path)
235
+ `#{settings[:file]} '#{file_path.shellescape}'`.strip
236
+ end
237
+
238
+ def validate
239
+ fail ArgumentError, "Header 'To:' is empty" if @to.empty?
240
+ warn 'Message body is empty' if @body.empty?
241
+ end
242
+
243
+ def new_boundary(type)
244
+ rand(36**6).to_s(36).rjust(20,type.to_s)
245
+ end
246
+
247
+ def encode_subject
248
+ @subject.ascii_only? ? @subject : "=?UTF-8?B?#{Base64.strict_encode64 @subject}?="
249
+ end
250
+ end
251
+ end
@@ -0,0 +1,59 @@
1
+ require 'tempfile'
2
+ require 'fileutils'
3
+
4
+ module SystemMail
5
+ ##
6
+ # A class to store string data either in StringIO or in Tempfile.
7
+ #
8
+ class Storage
9
+ def initialize(path = nil)
10
+ @tmpdir = path || Dir.tmpdir
11
+ @io = StringIO.new
12
+ end
13
+
14
+ def puts(data = nil)
15
+ @io.puts(data)
16
+ end
17
+
18
+ def read
19
+ if file?
20
+ capture do |file_path|
21
+ File.read file_path
22
+ end
23
+ else
24
+ @io.string.dup
25
+ end
26
+ end
27
+
28
+ def capture
29
+ ensure_tempfile
30
+ @io.close
31
+ yield @io.path
32
+ @io.open
33
+ end
34
+
35
+ def file?
36
+ @io.kind_of?(Tempfile)
37
+ end
38
+
39
+ def clear
40
+ @io.close
41
+ @io.unlink if file?
42
+ end
43
+
44
+ private
45
+
46
+ def ensure_tempfile
47
+ return if file?
48
+ tempfile = create_tempfile
49
+ tempfile.puts @io.string if @io.size > 0
50
+ @io = tempfile
51
+ end
52
+
53
+ def create_tempfile
54
+ temp_directory = File.join(@tmpdir, 'system_mail')
55
+ FileUtils.mkdir_p(temp_directory)
56
+ Tempfile.new('storage', temp_directory, :mode => IO::APPEND)
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,3 @@
1
+ module SystemMail
2
+ VERSION = "0.0.2"
3
+ end
@@ -0,0 +1,17 @@
1
+ require 'system_mail/version'
2
+ require 'system_mail/message'
3
+
4
+ module SystemMail
5
+ def self.new(options = {}, &block)
6
+ message = Message.new(options)
7
+ message.instance_eval(&block) if block_given?
8
+ message
9
+ end
10
+
11
+ def self.email(options = {}, &block)
12
+ message = SystemMail.new(options, &block)
13
+ message.deliver
14
+ end
15
+ end
16
+
17
+ Mail = SystemMail
@@ -0,0 +1,23 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'system_mail/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "system_mail"
8
+ spec.version = SystemMail::VERSION
9
+ spec.authors = ["Igor Bochkariov"]
10
+ spec.email = ["ujifgc@gmail.com"]
11
+ spec.description = 'A Ruby library built to compose and deliver internet mail using operating system utilities.'
12
+ spec.summary = 'SystemMail is a blazing-fast Ruby Mail alternative with tiny memory footprint.'
13
+ spec.homepage = "https://github.com/ujifgc/system_mail"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ end
@@ -0,0 +1 @@
1
+ <small>small</small> <big>big</big> нормал
@@ -0,0 +1,2 @@
1
+ require 'minitest/autorun'
2
+ require 'system_mail'
@@ -0,0 +1,53 @@
1
+ #coding:utf-8
2
+ require 'minitest_helper'
3
+
4
+ describe SystemMail::Storage do
5
+ before do
6
+ SystemMail::Storage.class_eval{ attr_accessor :io }
7
+ @fixture_path = File.expand_path('../fixtures/test.html', File.dirname(__FILE__))
8
+ @fixture = File.read @fixture_path
9
+ @s = SystemMail::Storage.new
10
+ end
11
+
12
+ after do
13
+ @s.clear
14
+ end
15
+
16
+ it 'should properly initialize' do
17
+ assert_kind_of StringIO, @s.io
18
+ end
19
+
20
+ it 'should properly write strings' do
21
+ 2.times do
22
+ @s.puts 'line1'
23
+ @s.puts 'а также линия'
24
+ @s.puts
25
+ end
26
+ assert_equal "line1\nа также линия\n\n"*2, @s.io.string
27
+ assert_kind_of StringIO, @s.io
28
+ end
29
+
30
+ it 'should properly write and append files with commandline' do
31
+ 2.times do
32
+ @s.capture do |path|
33
+ `cat '#{@fixture_path}' >> '#{path}'`
34
+ end
35
+ end
36
+ assert_equal @fixture*2, File.read(@s.io.path)
37
+ assert_kind_of Tempfile, @s.io
38
+ assert_match /system_mail(.*)storage/, @s.io.path
39
+ end
40
+
41
+ it 'should properly mix strings and files' do
42
+ 2.times do
43
+ @s.puts 'line1'
44
+ @s.puts 'а также линия'
45
+ @s.puts
46
+ @s.capture do |path|
47
+ File.open(path, 'a') { |f| f.write @fixture }
48
+ end
49
+ end
50
+ assert_equal ("line1\nа также линия\n\n"+@fixture)*2, File.read(@s.io.path)
51
+ assert_kind_of Tempfile, @s.io
52
+ end
53
+ end
@@ -0,0 +1 @@
1
+ require 'minitest_helper'
metadata ADDED
@@ -0,0 +1,99 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: system_mail
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.2
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Igor Bochkariov
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2014-01-27 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.3'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '1.3'
30
+ - !ruby/object:Gem::Dependency
31
+ name: rake
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ description: A Ruby library built to compose and deliver internet mail using operating
47
+ system utilities.
48
+ email:
49
+ - ujifgc@gmail.com
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - .gitignore
55
+ - .travis.yml
56
+ - Gemfile
57
+ - LICENSE.txt
58
+ - README.md
59
+ - Rakefile
60
+ - lib/system_mail.rb
61
+ - lib/system_mail/message.rb
62
+ - lib/system_mail/storage.rb
63
+ - lib/system_mail/version.rb
64
+ - system_mail.gemspec
65
+ - test/fixtures/test.html
66
+ - test/minitest_helper.rb
67
+ - test/system_mail/system_mail-storage_test.rb
68
+ - test/system_mail/system_mail_test.rb
69
+ homepage: https://github.com/ujifgc/system_mail
70
+ licenses:
71
+ - MIT
72
+ post_install_message:
73
+ rdoc_options: []
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ none: false
78
+ requirements:
79
+ - - ! '>='
80
+ - !ruby/object:Gem::Version
81
+ version: '0'
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ requirements: []
89
+ rubyforge_project:
90
+ rubygems_version: 1.8.25
91
+ signing_key:
92
+ specification_version: 3
93
+ summary: SystemMail is a blazing-fast Ruby Mail alternative with tiny memory footprint.
94
+ test_files:
95
+ - test/fixtures/test.html
96
+ - test/minitest_helper.rb
97
+ - test/system_mail/system_mail-storage_test.rb
98
+ - test/system_mail/system_mail_test.rb
99
+ has_rdoc: