cli_application 0.1.4 → 0.1.5

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.
@@ -0,0 +1,402 @@
1
+ # CliApplication::MailLib::Message - сборка сообщения электронной почты
2
+
3
+ module CliApplication
4
+ module MailLib
5
+ class Message
6
+ attr_reader :from_email
7
+ attr_accessor :from_name
8
+ attr_reader :reply_to
9
+ attr_accessor :subject
10
+ attr_accessor :charset
11
+ attr_accessor :body
12
+
13
+ # Конструктор инициализирует сообщение электронной почты и компоненты сообщения
14
+ #
15
+ # @return [None] нет
16
+ def initialize
17
+ @charset = 'utf-8'
18
+ @body = ''
19
+ @subject = ''
20
+ @from_email = ''
21
+ @from_name = ''
22
+ @reply_to = ''
23
+
24
+ @to = Hash.new
25
+ @cc = Hash.new
26
+ @bcc = Hash.new
27
+
28
+ @message_id = ::Time.now.to_s.hash.abs.to_s + '.' + ::Time.now.usec.to_s
29
+ end
30
+
31
+ # Метод осуществляет сборку (композицию) сообщения в формате MIME для записи в лог файл
32
+ # без преобразований base64
33
+ #
34
+ # @return [String] сообщение электронной почты в виде форматированного текста
35
+ def to_log
36
+ message = Array.new
37
+ message << "From: #{build_rfc822_name(@from_email, @from_name, false)}" unless @from_email == ''
38
+ message << build_to_adresses("To", @to, false)
39
+ message << build_to_adresses("Cc", @cc, false)
40
+ message << build_to_adresses("Bcc", @bcc, false)
41
+ message << "Reply-To: #{build_rfc822_name(@reply_to)}" unless @reply_to == ''
42
+ message << "Subject: #{@subject}"
43
+ message << html_to_text(@body.dup, 65, @charset)
44
+
45
+ message.compact!
46
+ message.join("\n")
47
+ end
48
+
49
+ # Метод осуществляет сборку (композицию) сообщения в формате MIME для отправки в Интернет.
50
+ # Поля TO, CC, BCC, Subject преобразуются в бинарную форму через base64
51
+ #
52
+ # @return [String] сообщение электронной почты в виде форматированного текста
53
+ def to_s
54
+ message = Array.new
55
+ message << "From: #{build_rfc822_name(@from_email, @from_name)}" unless @from_email == ''
56
+ message << "Return-Path: <#{@from_email}>" # http://maksd.info/blog/vse-posty-iz-starogo-bloga/message-75/
57
+ message << build_to_adresses("To", @to)
58
+ message << build_to_adresses("Cc", @cc)
59
+ message << build_to_adresses("Bcc", @bcc)
60
+ message << "Reply-To: #{build_rfc822_name(@reply_to)}" unless @reply_to == ''
61
+ message << "Subject: #{base64_string_encode(@subject)}"
62
+ message << "Date: #{::Time.zone.now.to_formatted_s(:rfc822) }"
63
+ message << "MIME-Version: 1.0 (Ruby gem cli_application; version #{::CliApplication::VERSION})"
64
+ message << "Message-ID: <#{@message_id + '@' + @from_email.split('@').last}>"
65
+ message += alternative_to_s
66
+ message += body_to_s(html_to_text(@body.dup, 65, @charset), 'text/plain')
67
+ message += body_to_s(@body, 'text/html')
68
+ message += footer_to_s
69
+
70
+ message.compact!
71
+ message.join("\n")
72
+ end
73
+
74
+ # Метод добавляет к сообщению указание на адрес и имя отправителя. Принимается формат вида
75
+ # "Name <name@host.ru>". При этом будет осуществлен корректный разбор строки на имя и адрес
76
+ #
77
+ # @param [String] val строка с адресом электронной почты
78
+ # @return [None] нет
79
+ # @example Примеры использования
80
+ # msg = CliApplication::MailLib::Message.new
81
+ # msg.from_email = "Name <user@host.ru>"
82
+ # msg.from_email #=> "user@host.ru"
83
+ # msg.from_name #=> "Name"
84
+ #
85
+ # msg.from_email = "user@host.ru"
86
+ # msg.from_email #=> "user@host.ru"
87
+ # msg.from_name #=> ""
88
+ def from_email=(val)
89
+ res = parse_email(val)
90
+ @from_name = res[:name]
91
+ @from_email = res[:email]
92
+ end
93
+
94
+ # Метод добавляет к сообщению указание на адрес для ответа. Принимается формат вида
95
+ # "Name <name@host.ru>". При этом будет осуществлен корректный разбор строки на имя и адрес
96
+ #
97
+ # @param [String] val строка с адресом электронной почты
98
+ # @return [None] нет
99
+ # @example Примеры использования
100
+ # msg = CliApplication::MailLib::Message.new
101
+ # msg.reply_to = "Name <user@host.ru>"
102
+ # msg.reply_to #=> "user@host.ru"
103
+ def reply_to=(val)
104
+ res = parse_email(val)
105
+ @reply_to = res[:email]
106
+ end
107
+
108
+ # Метод добавляет в поле TO получателя сообщения. Может вызываться несколько раз для
109
+ # добавления нескольких получателей. Особенности обработки. Если в метод передать значения адреса, включающего
110
+ # имя пользователя, то параметр name будет проигнорирован. Name будет взят из переданного адреса.
111
+ #
112
+ # @param [String] email адрес получателя в формате "user@host.ru" или "Name <user@host.ru>"
113
+ # @param [String] name имя пользователя
114
+ # @return [None] нет
115
+ # @example Примеры использования
116
+ # msg = CliApplication::MailLib::Message.new
117
+ # msg.add_to('user@host.ru', 'Name') #=> добавлено: "Name" и "user@host.ru"
118
+ # msg.add_to('USerName <user@host.ru>', 'Name') #=> добавлено: "UserName" и "user@host.ru"
119
+ def add_to(email, name = '')
120
+ res = parse_email(email)
121
+ if name == ''
122
+ @to[res[:email]] = res[:name]
123
+ else
124
+ @to[res[:email]] = name
125
+ end
126
+ end
127
+
128
+ # Метод добавляет в поле CC получателя сообщения. Может вызываться несколько раз для
129
+ # добавления нескольких получателей. Особенности обработки. Если в метод передать значения адреса, включающего
130
+ # имя пользователя, то параметр name будет проигнорирован. Name будет взят из переданного адреса. Вторая
131
+ # особенность - при использовании метода отправки :smtp, все CC-адреса будут помещены в TO.
132
+ #
133
+ # @param [String] email адрес получателя в формате "user@host.ru" или "Name <user@host.ru>"
134
+ # @param [String] name имя пользователя
135
+ # @return [None] нет
136
+ # @example Примеры использования
137
+ # msg = CliApplication::MailLib::Message.new
138
+ # msg.add_cc('user@host.ru', 'Name') #=> добавлено: "Name" и "user@host.ru"
139
+ # msg.add_cc('USerName <user@host.ru>', 'Name') #=> добавлено: "UserName" и "user@host.ru"
140
+ def add_cc(email, name = '')
141
+ res = parse_email(email)
142
+ if name == ''
143
+ @cc[res[:email]] = res[:name]
144
+ else
145
+ @cc[res[:email]] = name
146
+ end
147
+ end
148
+
149
+ # Метод добавляет в поле BCC получателя сообщения. Может вызываться несколько раз для
150
+ # добавления нескольких получателей. Особенности обработки. Если в метод передать значения адреса, включающего
151
+ # имя пользователя, то параметр name будет проигнорирован. Name будет взят из переданного адреса. Вторая
152
+ # особенность - при использовании метода отправки :smtp, все BCC-адреса будут удалены.
153
+ #
154
+ # @param [String] email адрес получателя в формате "user@host.ru" или "Name <user@host.ru>"
155
+ # @param [String] name имя пользователя
156
+ # @return [None] нет
157
+ # @example Примеры использования
158
+ # msg = CliApplication::MailLib::Message.new
159
+ # msg.add_bcc('user@host.ru', 'Name') #=> добавлено: "Name" и "user@host.ru"
160
+ # msg.add_bcc('USerName <user@host.ru>', 'Name') #=> добавлено: "UserName" и "user@host.ru"
161
+ def add_bcc(email, name = '')
162
+ res = parse_email(email)
163
+ if name == ''
164
+ @bcc[res[:email]] = res[:name]
165
+ else
166
+ @bcc[res[:email]] = name
167
+ end
168
+ end
169
+
170
+ # Метод очищает все ранее добавленные адреса TO.
171
+ #
172
+ # @param [Boolean] warning true для вывода предупреждения об удалении всех адресатов
173
+ # @return [None] нет
174
+ def clear_to(warning = false)
175
+ unless @to.empty?
176
+ if warning
177
+ warn "Предупреждение: TO-адреса #{@to.inspect} удалены"
178
+ end
179
+ @to = Hash.new
180
+ end
181
+ end
182
+
183
+ # Метод очищает все ранее добавленные адреса CC.
184
+ #
185
+ # @param [Boolean] warning true для вывода предупреждения об удалении всех адресатов
186
+ # @return [None] нет
187
+ def clear_cc(warning = false)
188
+ unless @cc.empty?
189
+ if warning
190
+ warn "Предупреждение: CC-адреса #{@cc.inspect} удалены"
191
+ end
192
+ @cc = Hash.new
193
+ end
194
+ end
195
+
196
+ # Метод очищает все ранее добавленные адреса BCC.
197
+ #
198
+ # @param [Boolean] warning true для вывода предупреждения об удалении всех адресатов
199
+ # @return [None] нет
200
+ def clear_bcc(warning = false)
201
+ unless @bcc.empty?
202
+ if warning
203
+ warn "Предупреждение: BCC-адреса #{@bcc.inspect} удалены"
204
+ end
205
+ @bcc = Hash.new
206
+ end
207
+ end
208
+
209
+
210
+
211
+ private
212
+
213
+
214
+
215
+ def to_emails # :nodoc:
216
+ out = build_to_adresses('', @to).gsub(/\A\:/, '')
217
+ out += ',' + build_to_adresses('', @cc).gsub(/\A\:/, '') unless @cc.empty?
218
+ StTools::String.split(out, ',')
219
+ end
220
+
221
+ def base64_string_encode(str) # :nodoc:
222
+ "=?UTF-8?B?" + Base64.strict_encode64(str) + "?="
223
+ end
224
+
225
+ def build_to_adresses(prefix, to, base64 = true) # :nodoc:
226
+ return nil if to.empty?
227
+
228
+ out = Array.new
229
+ to.each do |email, name|
230
+ out << build_rfc822_name(email, name, base64)
231
+ end
232
+
233
+ "#{prefix}: #{out.join(", ")}"
234
+ end
235
+
236
+ def build_rfc822_name(email, name, base64 = true) # :nodoc:
237
+ return "#{email}" if name.nil? || name == ''
238
+ if base64
239
+ "#{base64_string_encode(name)} <#{email}>"
240
+ else
241
+ "#{name.inspect} <#{email}>"
242
+ end
243
+ end
244
+
245
+ def parse_email(str) # :nodoc:
246
+ name = str.strip.match(/^.+\</)[0] rescue nil
247
+ if name.nil?
248
+ {name: '', email: str.strip}
249
+ else
250
+ {name: name.chomp('<').strip, email: str.strip.gsub(name, '').chomp('>')}
251
+ end
252
+ end
253
+
254
+ # https://github.com/premailer/premailer/blob/master/lib/premailer/html_to_plain_text.rb
255
+ def html_to_text(html, line_length = 65, from_charset = 'UTF-8') # :nodoc:
256
+ txt = html
257
+
258
+ # strip text ignored html. Useful for removing
259
+ # headers and footers that aren't needed in the
260
+ # text version
261
+ txt.gsub!(/<!-- start text\/html -->.*?<!-- end text\/html -->/m, '')
262
+
263
+ # replace images with their alt attributes
264
+ # for img tags with "" for attribute quotes
265
+ # with or without closing tag
266
+ # eg. the following formats:
267
+ # <img alt="" />
268
+ # <img alt="">
269
+ txt.gsub!(/<img.+?alt=\"([^\"]*)\"[^>]*\>/i, '\1')
270
+
271
+ # for img tags with '' for attribute quotes
272
+ # with or without closing tag
273
+ # eg. the following formats:
274
+ # <img alt='' />
275
+ # <img alt=''>
276
+ txt.gsub!(/<img.+?alt=\'([^\']*)\'[^>]*\>/i, '\1')
277
+
278
+ # links
279
+ txt.gsub!(/<a\s.*?href=\"(mailto:)?([^\"]*)\"[^>]*>((.|\s)*?)<\/a>/i) do |s|
280
+ if $3.empty?
281
+ ''
282
+ else
283
+ $3.strip + ' ( ' + $2.strip + ' )'
284
+ end
285
+ end
286
+
287
+ txt.gsub!(/<a\s.*?href='(mailto:)?([^\']*)\'[^>]*>((.|\s)*?)<\/a>/i) do |s|
288
+ if $3.empty?
289
+ ''
290
+ else
291
+ $3.strip + ' ( ' + $2.strip + ' )'
292
+ end
293
+ end
294
+
295
+ # handle headings (H1-H6)
296
+ txt.gsub!(/(<\/h[1-6]>)/i, "\n\\1") # move closing tags to new lines
297
+ txt.gsub!(/[\s]*<h([1-6]+)[^>]*>[\s]*(.*)[\s]*<\/h[1-6]+>/i) do |s|
298
+ hlevel = $1.to_i
299
+
300
+ htext = $2
301
+ htext.gsub!(/<br[\s]*\/?>/i, "\n") # handle <br>s
302
+ htext.gsub!(/<\/?[^>]*>/i, '') # strip tags
303
+
304
+ # determine maximum line length
305
+ hlength = 0
306
+ htext.each_line { |l| llength = l.strip.length; hlength = llength if llength > hlength }
307
+ hlength = line_length if hlength > line_length
308
+
309
+ case hlevel
310
+ when 1 # H1, asterisks above and below
311
+ htext = ('*' * hlength) + "\n" + htext + "\n" + ('*' * hlength)
312
+ when 2 # H1, dashes above and below
313
+ htext = ('-' * hlength) + "\n" + htext + "\n" + ('-' * hlength)
314
+ else # H3-H6, dashes below
315
+ htext = htext + "\n" + ('-' * hlength)
316
+ end
317
+
318
+ "\n\n" + htext + "\n\n"
319
+ end
320
+
321
+ # wrap spans
322
+ txt.gsub!(/(<\/span>)[\s]+(<span)/mi, '\1 \2')
323
+
324
+ # lists -- TODO: should handle ordered lists
325
+ txt.gsub!(/[\s]*(<li[^>]*>)[\s]*/i, '* ')
326
+ # list not followed by a newline
327
+ txt.gsub!(/<\/li>[\s]*(?![\n])/i, "\n")
328
+
329
+ # paragraphs and line breaks
330
+ txt.gsub!(/<\/p>/i, "\n\n")
331
+ txt.gsub!(/<br[\/ ]*>/i, "\n")
332
+
333
+ # strip remaining tags
334
+ txt.gsub!(/<\/?[^>]*>/, '')
335
+
336
+ # decode HTML entities
337
+ he = HTMLEntities.new
338
+ txt = he.decode(txt)
339
+
340
+ txt = word_wrap(txt, line_length)
341
+
342
+ # remove linefeeds (\r\n and \r -> \n)
343
+ txt.gsub!(/\r\n?/, "\n")
344
+
345
+ # strip extra spaces
346
+ txt.gsub!(/\302\240+/, " ") # non-breaking spaces -> spaces
347
+ txt.gsub!(/\n[ \t]+/, "\n") # space at start of lines
348
+ txt.gsub!(/[ \t]+\n/, "\n") # space at end of lines
349
+
350
+ # no more than two consecutive newlines
351
+ txt.gsub!(/[\n]{3,}/, "\n\n")
352
+
353
+ # no more than two consecutive spaces
354
+ txt.gsub!(/ {2,}/, " ")
355
+
356
+ # the word messes up the parens
357
+ txt.gsub!(/\([ \n](http[^)]+)[\n ]\)/) do |s|
358
+ "( " + $1 + " )"
359
+ end
360
+
361
+ txt.strip
362
+ end
363
+
364
+ # Taken from Rails' word_wrap helper (http://api.rubyonrails.org/classes/ActionView/Helpers/TextHelper.html#method-i-word_wrap)
365
+ def word_wrap(txt, line_length) # :nodoc:
366
+ txt.split("\n").collect do |line|
367
+ line.length > line_length ? line.gsub(/(.{1,#{line_length}})(\s+|$)/, "\\1\n").strip : line
368
+ end * "\n"
369
+ end
370
+
371
+ def boundary # :nodoc:
372
+ 'NextPart_' + @message_id.gsub(/\./, '_')
373
+ end
374
+
375
+ def alternative_to_s # :nodoc:
376
+ message = Array.new
377
+ message << "Content-Type: multipart/alternative; boundary=\"#{boundary}\""
378
+ message
379
+ end
380
+
381
+ def body_to_s(text, type) # :nodoc:
382
+ message = Array.new
383
+ message << ""
384
+ message << "--#{boundary}"
385
+ message << "Content-Transfer-Encoding: 8bit"
386
+ message << "Content-Type: #{type}; charset=\"#{@charset}\""
387
+ message << ""
388
+ message << text
389
+ message
390
+ end
391
+
392
+ def footer_to_s # :nodoc:
393
+ message = Array.new
394
+ message << ""
395
+ message << "--#{boundary}--"
396
+ message << ""
397
+ message
398
+ end
399
+
400
+ end
401
+ end
402
+ end
@@ -0,0 +1,89 @@
1
+ # CliApplication::MailLib::Sendmail - класс отправки электронной почты через Sendmail
2
+
3
+ module CliApplication
4
+ module MailLib
5
+ class Sendmail < ::CliApplication::MailLib::Base
6
+
7
+ def initialize(config, folders) # :nodoc:
8
+ @delivery_method = :sendmail
9
+ super(config, folders)
10
+
11
+ check_config
12
+ end
13
+
14
+ # Метод отправки электронного сообщения через sendmail
15
+ #
16
+ # @param [String] to электронная почта лица, которому отправляется сообщение, или массив адресов
17
+ # @param [String] name имя клиента, которому отправляется сообщение
18
+ # @param [String] title заголовок письма
19
+ # @param [String] body текст письма
20
+ # @return [Boolean] true, если письмо помещено в очередь сообщений sendmail
21
+ def simple_send(to, name, title, body)
22
+ return false unless valid?
23
+
24
+ message = CliApplication::MailLib::Message.new
25
+ message.from_email = @config.from
26
+ message.subject = title
27
+ message.body = (@config.footer.nil? || @config.footer == '') ? body : (body+@config.footer)
28
+ processing_to(to, name, message)
29
+
30
+ begin
31
+ send_message(message)
32
+ true
33
+ rescue Exception => e
34
+ $stderr.puts "Ошибка отправки письма: #{e.message}"
35
+ false
36
+ end
37
+ end
38
+
39
+ # Метод возвращает путь к скрипту 'sendmail'
40
+ #
41
+ # @return [String] путь к скрипту 'sendmail'
42
+ def sendmail_location
43
+ @config.sendmail.location
44
+ end
45
+
46
+ # Метод возаращает опции 'sendmail'
47
+ #
48
+ # @return [String] опции 'sendmail'
49
+ def sendmail_arguments
50
+ @config.sendmail.arguments
51
+ end
52
+
53
+
54
+ private
55
+
56
+
57
+
58
+ def send_message(message) # :nodoc:
59
+ # Дополнительная информация по использованию sendmail:
60
+ # http://blog.antage.name/posts/sendmail-in-rails.html
61
+
62
+ cmdline = Array.new
63
+ cmdline << "echo \"#{'rrrrr' + message.to_s}\""
64
+ cmdline << '|'
65
+ cmdline << sendmail_location
66
+ cmdline << sendmail_arguments
67
+ cmdline << '2>&1'
68
+ cmdline = cmdline.join(' ')
69
+
70
+ output = `#{cmdline}`
71
+ res = $?.exitstatus
72
+
73
+ if res != 0
74
+ raise "SendmailError_#{res}"
75
+ end
76
+ end
77
+
78
+ def check_config # :nodoc:
79
+ return set_check_config_state(false, "Не найдена секция конфиг-файла cli/mail/sendmail") if @config.sendmail.nil?
80
+ return set_check_config_state(false, "Не найден параметр конфиг-файла cli/mail/sendmail/location") if @config.sendmail.location.nil?
81
+ return set_check_config_state(false, "Не найден параметр конфиг-файла cli/mail/sendmail/arguments") if @config.sendmail.arguments.nil?
82
+ return set_check_config_state(false, "Не найден sendmail (#{@config.sendmail.location.inspect})") unless File.exist?(@config.sendmail.location)
83
+
84
+ set_check_config_state(true, '')
85
+ end
86
+
87
+ end
88
+ end
89
+ end