maildis 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. data/.gitignore +18 -0
  2. data/.rspec +2 -0
  3. data/Gemfile +2 -0
  4. data/Guardfile +24 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +134 -0
  7. data/Rakefile +11 -0
  8. data/bin/maildis +5 -0
  9. data/lib/maildis/dispatcher.rb +43 -0
  10. data/lib/maildis/merge_field.rb +9 -0
  11. data/lib/maildis/multi_io.rb +15 -0
  12. data/lib/maildis/recipient.rb +22 -0
  13. data/lib/maildis/recipient_parser.rb +87 -0
  14. data/lib/maildis/sender.rb +17 -0
  15. data/lib/maildis/server_utils.rb +20 -0
  16. data/lib/maildis/smtp_server.rb +13 -0
  17. data/lib/maildis/template.rb +17 -0
  18. data/lib/maildis/template_loader.rb +18 -0
  19. data/lib/maildis/template_renderer.rb +20 -0
  20. data/lib/maildis/validation_error.rb +3 -0
  21. data/lib/maildis/validation_utils.rb +19 -0
  22. data/lib/maildis/validator.rb +50 -0
  23. data/lib/maildis/version.rb +3 -0
  24. data/lib/maildis.rb +118 -0
  25. data/maildis.gemspec +33 -0
  26. data/spec/lib/maildis/dispatcher_spec.rb +31 -0
  27. data/spec/lib/maildis/maildis_spec.rb +84 -0
  28. data/spec/lib/maildis/merge_field_spec.rb +15 -0
  29. data/spec/lib/maildis/recipient_parser_spec.rb +39 -0
  30. data/spec/lib/maildis/recipient_spec.rb +16 -0
  31. data/spec/lib/maildis/sender_spec.rb +15 -0
  32. data/spec/lib/maildis/smtp_server_spec.rb +17 -0
  33. data/spec/lib/maildis/template_loader_spec.rb +25 -0
  34. data/spec/lib/maildis/template_renderer_spec.rb +26 -0
  35. data/spec/lib/maildis/template_spec.rb +15 -0
  36. data/spec/lib/maildis/validator_spec.rb +132 -0
  37. data/spec/mailer/empty.csv +0 -0
  38. data/spec/mailer/invalid.csv +1 -0
  39. data/spec/mailer/mailer.yml +15 -0
  40. data/spec/mailer/mailer_invalid.yml +1 -0
  41. data/spec/mailer/recipients.csv +1 -0
  42. data/spec/mailer/recipients_empty.csv +1 -0
  43. data/spec/mailer/template.html +13 -0
  44. data/spec/mailer/template.txt +5 -0
  45. data/spec/spec_helper.rb +2 -0
  46. metadata +291 -0
data/.gitignore ADDED
@@ -0,0 +1,18 @@
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
18
+ .rvmrc
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format Fivemat
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source 'https://rubygems.org'
2
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,24 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec' do
5
+ watch(%r{^spec/.+_spec\.rb$})
6
+ watch(%r{^lib/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+
9
+ # Rails example
10
+ watch(%r{^app/(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
11
+ watch(%r{^app/(.*)(\.erb|\.haml)$}) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
12
+ watch(%r{^app/controllers/(.+)_(controller)\.rb$}) { |m| ["spec/routing/#{m[1]}_routing_spec.rb", "spec/#{m[2]}s/#{m[1]}_#{m[2]}_spec.rb", "spec/acceptance/#{m[1]}_spec.rb"] }
13
+ watch(%r{^spec/support/(.+)\.rb$}) { "spec" }
14
+ watch('config/routes.rb') { "spec/routing" }
15
+ watch('app/controllers/application_controller.rb') { "spec/controllers" }
16
+
17
+ # Capybara features specs
18
+ watch(%r{^app/views/(.+)/.*\.(erb|haml)$}) { |m| "spec/features/#{m[1]}_spec.rb" }
19
+
20
+ # Turnip features and steps
21
+ watch(%r{^spec/acceptance/(.+)\.feature$})
22
+ watch(%r{^spec/acceptance/steps/(.+)_steps\.rb$}) { |m| Dir[File.join("**/#{m[1]}.feature")][0] || 'spec/acceptance' }
23
+ end
24
+
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2012 Johnny Boursiquot
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,134 @@
1
+ # Maildis
2
+
3
+ [![Code
4
+ Climate](https://codeclimate.com/badge.png)](https://codeclimate.com/github/jboursiquot/maildis) [![Dependency
5
+ Status](https://gemnasium.com/jboursiquot/maildis.png)](https://gemnasium.com/jboursiquot/maildis)
6
+
7
+ Maildis is a command line bulk email dispatching tool. It supports HTML and plain text templates and CSVs for recipients and merge fields. It relies on SMTP information you provide through your own configuration file. Subject, sender, path to CSV and path to the templates are all configurable through YAML.
8
+
9
+ ## Installation
10
+
11
+ $ gem install maildis
12
+
13
+ ## Usage
14
+
15
+ ### Maildis Commands:
16
+
17
+ maildis dispatch mailer # Dispatches the mailer through the SMTP server specified in the mailer configuration.
18
+ maildis ping mailer # Attempts to connect to the SMTP server specified in the mailer configuration
19
+ maildis validate mailer # Validates mailer configuration file
20
+
21
+ In the above task listing, _mailer_ refers to a YAML configuration file
22
+ for your mailer. You may get additional help on any of the available tasks, for example:
23
+
24
+ $ maildis help dispath
25
+
26
+ Usage:
27
+ maildis ping mailer
28
+
29
+ Options:
30
+ -v, [--verbose=VERBOSE] # Verbose
31
+
32
+ ## Creating and Dispatching Mailers
33
+
34
+ ### 1. Create a mailer configuration (e.g. mailer.yml)
35
+
36
+ mailer:
37
+ subject: "Test Mailer"
38
+ sender:
39
+ name: "Developer"
40
+ email: "developer@company.com"
41
+ recipients:
42
+ csv: "/path/to/recipients.csv"
43
+ templates:
44
+ html: "/path/to/template.html"
45
+ plain: "/path/to/template.txt"
46
+ smtp:
47
+ host: "my.mail.host"
48
+ port: 25
49
+ username: "username"
50
+ password: "password"
51
+
52
+ ### 2. Set up html and plain text templates, for example:
53
+
54
+ **HTML**
55
+
56
+ <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
57
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
58
+ <html>
59
+ <head>
60
+ <title>Test Mailer</title>
61
+ </head>
62
+ <body>
63
+ <p>This is a test mailer</p>
64
+ <p>The following merge field should be replaced with an actual URL: <a href="%url%">%url%</a></p>
65
+ <p>Organization | Address | Unsubscribe Link</p>
66
+ </body>
67
+ </html>
68
+
69
+ **Plain**
70
+
71
+ This is a test mailer.
72
+
73
+ The following merge field should be replaced with an actual URL: %url%
74
+
75
+ Organization | Address | Unsubscribe Link
76
+
77
+ Note the _%url%_ markers in the template files. This %{string}%
78
+ mechanism allows you to specify merge fields which are replaced by their
79
+ equivalent columns from your recipients CSV. In this case, the CSV example below,
80
+ lists a _url_ column that will be used as a merge field.
81
+
82
+ ### 3. Prepare your recipients CSV, for example:
83
+
84
+ name,email,url
85
+ Johnny Boursiquot,jboursiquot@gmail.com,https://github.com/jboursiquot
86
+ Johnny Boursiquot,jboursiquot@maark.com,http://www.maark.com
87
+
88
+ ### 4. Validate your configuration
89
+
90
+ $ maildis validate /path/to/your/mailer.yml
91
+
92
+ This will go through the settings in your mailer configuration and check
93
+
94
+ - sender email validity
95
+ - template path validity
96
+ - recipients CSV path and format validity
97
+ - SMTP server reachability
98
+
99
+ You can test the reachability of the SMTP server defined in your mailer configuration on its own by issueing the following command:
100
+
101
+ $ maildis ping /path/to/your/mailer.yml
102
+
103
+ ### 5. Invoke the maildis command while passing in the path to your
104
+ mailer configuration to its dispatch task
105
+
106
+ $ maildis dispath /path/to/your/mailer.yml -v
107
+
108
+ When _maildis_ processes your _dispatch_ request, a logfile (.maildis.log) is written in the user's home directory. In the above command, the _dispatch_ task is invoked with the _verbose_ flag which will output to STDOUT as well as ~/.maildis.log. Example output based on the recipients.csv above might look like this:
109
+
110
+ 2012-12-11 20:18:10 -0500 INFO: Load /path/to/your/mailer.yml
111
+ 2012-12-11 20:18:10 -0500 INFO: Validate configuration
112
+ 2012-12-11 20:18:10 -0500 INFO: Validate recipients csv
113
+ 2012-12-11 20:18:10 -0500 INFO: Extract recipients from csv
114
+ 2012-12-11 20:18:10 -0500 INFO: Validate sender
115
+ 2012-12-11 20:18:10 -0500 INFO: Validate templates
116
+ 2012-12-11 20:18:10 -0500 INFO: Validate SMTP server localhost
117
+ 2012-12-11 20:18:10 -0500 INFO: Dispatching to 2 recipients
118
+ 2012-12-11 20:18:10 -0500 INFO: Sent: Johnny Boursiquot <jboursiquot@gmail.com>
119
+ 2012-12-11 20:18:10 -0500 INFO: Sent: Johnny Boursiquot <jboursiquot@maark.com>
120
+ 2012-12-11 20:18:11 -0500 INFO: Dispatch complete without errors
121
+
122
+ ## Local SMTP Server For Testing
123
+
124
+ I recommend you use the _mailcatcher_ gem to get a local SMTP server up
125
+ and running while you test. This will speed up your testing and even
126
+ require network access. See http://mailcatcher.me for more details.
127
+
128
+ ## Contributing
129
+
130
+ 1. Fork it
131
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
132
+ 3. Commit your changes with tests (`git commit -am 'Add some feature'`)
133
+ 4. Push to the branch (`git push origin my-new-feature`)
134
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core'
3
+ require 'rspec/core/rake_task'
4
+
5
+ task :default => [:spec]
6
+
7
+ desc 'Runs all specs'
8
+ RSpec::Core::RakeTask.new(:spec) do |spec|
9
+ spec.pattern = "spec/**/*_spec.rb"
10
+ spec.rspec_opts = ["--format Fivemat", "--color"]
11
+ end
data/bin/maildis ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require_relative '../lib/maildis'
4
+
5
+ Maildis::CLI.start
@@ -0,0 +1,43 @@
1
+ require "pony"
2
+ require "logger"
3
+
4
+ module Maildis
5
+
6
+ class Dispatcher
7
+
8
+ class << self
9
+
10
+ def dispatch(options = {})
11
+ result = {sent: [], not_sent: []}
12
+ options[:recipients].each do |recipient|
13
+ begin
14
+ html_body = TemplateRenderer.render(options[:templates][:html], recipient.merge_fields)
15
+ plain_body = TemplateRenderer.render(options[:templates][:plain], recipient.merge_fields)
16
+ Pony.mail({
17
+ to: recipient.to_email,
18
+ from: options[:sender].to_email,
19
+ subject: options[:subject],
20
+ html_body: html_body,
21
+ body: plain_body,
22
+ via: :smtp,
23
+ via_options: {address: options[:server].host,
24
+ port: options[:server].port,
25
+ user_name: options[:server].username,
26
+ password: options[:server].password}
27
+ })
28
+ options[:logger].info "Sent: #{recipient.to_email}" if options[:logger]
29
+ result[:sent] << recipient
30
+ rescue => e
31
+ options[:logger].error "Error: #{recipient.to_email} | #{e.message}" if options[:logger]
32
+ result[:not_sent] << {recipient: recipient, reason: e.message}
33
+ end
34
+ end
35
+ result
36
+ end
37
+
38
+ end
39
+
40
+ end
41
+
42
+
43
+ end
@@ -0,0 +1,9 @@
1
+ module Maildis
2
+ class MergeField
3
+ attr_reader :field, :value
4
+ def initialize(field, value)
5
+ @field = field
6
+ @value = value || ""
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,15 @@
1
+ class MultiIO
2
+
3
+ def initialize(*targets)
4
+ @targets = targets
5
+ end
6
+
7
+ def write(*args)
8
+ @targets.each {|t| t.write(*args)}
9
+ end
10
+
11
+ def close
12
+ @targets.each {|t| t.close}
13
+ end
14
+
15
+ end
@@ -0,0 +1,22 @@
1
+ module Maildis
2
+ class Recipient
3
+ attr_reader :name, :email
4
+ attr_accessor :merge_fields
5
+
6
+ def initialize(name, email, merge_fields=[])
7
+ @name = name
8
+ @email = email
9
+ @merge_fields = merge_fields
10
+ end
11
+
12
+ def to_email
13
+ return "#{@name} <#{@email}>" if @name
14
+ @email
15
+ end
16
+
17
+ def to_s
18
+ "<Recipient #{name} | #{email}>"
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,87 @@
1
+ require_relative 'validation_error'
2
+ require_relative 'recipient'
3
+ require 'csv'
4
+
5
+ module Maildis
6
+
7
+ class RecipientParser
8
+
9
+ class << self
10
+
11
+ @@expected_columns = [:name, :email]
12
+
13
+ def valid_csv?(file_path)
14
+ csv = extract_data file_path
15
+ begin
16
+ validate_headers(normalize_headers(csv.headers), @@expected_columns)
17
+ true
18
+ rescue ValidationError => e
19
+ false
20
+ end
21
+ end
22
+
23
+ def empty_csv?(file_path)
24
+ csv = extract_data file_path
25
+ csv.empty?
26
+ end
27
+
28
+ def extract_recipients(file_path)
29
+ validate_file_path file_path
30
+ csv = extract_data file_path
31
+ validate_headers(normalize_headers(csv.headers), @@expected_columns)
32
+ parse_csv(csv)
33
+ end
34
+
35
+ def extract_data(file_path)
36
+ begin
37
+ CSV.read(file_path, {headers: true})
38
+ rescue ArgumentError => e
39
+ begin
40
+ # Force UTF-8
41
+ data = IO.read(file_path).force_encoding("ISO-8859-1").encode("utf-8", replace: nil)
42
+ CSV.parse(data, {headers: true})
43
+ rescue => e
44
+ raise "Failed to parse CSV. Unable to force UTF-8 conversion."+ " " + e.message
45
+ end
46
+ end
47
+ end
48
+
49
+ def parse_csv(csv)
50
+ result = []
51
+ csv.each do |r|
52
+ row = {}
53
+ r.to_hash.each_pair {|k,v| row.merge!({k.downcase => v})}
54
+ result << recipient_instance(row)
55
+ end
56
+ result
57
+ end
58
+
59
+ def recipient_instance(row)
60
+ recipient = Recipient.new row['name'], row['email']
61
+ row.each do |key, value|
62
+ recipient.merge_fields << MergeField.new(key, value) unless %w{name email}.include? key
63
+ end
64
+ recipient
65
+ end
66
+
67
+ def parse_file(file_path)
68
+ IO.read(file_path).force_encoding("ISO-8859-1").encode("utf-8", replace: nil)
69
+ end
70
+
71
+ def normalize_headers(headers)
72
+ headers.map(&:downcase)
73
+ end
74
+
75
+ def validate_file_path(file_path)
76
+ raise ValidationError, "File not found: #{file_path}" unless File.exists?(file_path)
77
+ end
78
+
79
+ def validate_headers(headers, expected)
80
+ expected.each do |header|
81
+ raise ValidationError, "Missing '#{header.to_s}' column" unless headers.include? header.to_s
82
+ end
83
+ end
84
+ end
85
+ end
86
+
87
+ end
@@ -0,0 +1,17 @@
1
+ module Maildis
2
+
3
+ class Sender
4
+ attr_reader :name, :email
5
+
6
+ def initialize(name, email)
7
+ @name = name
8
+ @email = email
9
+ end
10
+
11
+ def to_email
12
+ return "#{@name} <#{@email}>" if @name
13
+ @email
14
+ end
15
+ end
16
+
17
+ end
@@ -0,0 +1,20 @@
1
+ require 'socket'
2
+
3
+ class ServerUtils
4
+
5
+ class << self
6
+
7
+ def server_reachable?(host, port=25)
8
+ begin
9
+ TCPSocket.new(host, port).close
10
+ true
11
+ rescue Errno::ECONNREFUSED
12
+ false
13
+ rescue => e
14
+ $stderr.puts "Error during mail server status check: " + e.message
15
+ end
16
+ end
17
+
18
+ end
19
+
20
+ end
@@ -0,0 +1,13 @@
1
+ module Maildis
2
+
3
+ class SmtpServer
4
+ attr_reader :host, :port, :username, :password
5
+ def initialize(host, port, username, password)
6
+ @host = host
7
+ @port = port
8
+ @username = username
9
+ @password = password
10
+ end
11
+ end
12
+
13
+ end
@@ -0,0 +1,17 @@
1
+ module Maildis
2
+ class Template
3
+
4
+ HTML = "html"
5
+ HTML_EXT = ".html"
6
+ PLAIN = "plain"
7
+ PLAIN_EXT = ".txt"
8
+
9
+ attr_reader :type, :content
10
+
11
+ def initialize(type, content)
12
+ @type = type
13
+ @content = content
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,18 @@
1
+ module Maildis
2
+
3
+ class TemplateLoader
4
+
5
+ class << self
6
+
7
+ def load(template_path)
8
+ path = File.expand_path(template_path)
9
+ type = File.extname(path) == Template::PLAIN_EXT ? Template::PLAIN : Template::HTML
10
+ content = File.read path
11
+ Template.new type, content
12
+ end
13
+
14
+ end
15
+
16
+ end
17
+
18
+ end
@@ -0,0 +1,20 @@
1
+ module Maildis
2
+
3
+ class TemplateRenderer
4
+
5
+ class << self
6
+
7
+ def render(template, merge_fields)
8
+ result = String.new(template.content)
9
+ merge_fields.each do |mf|
10
+ token = /#{Regexp.quote('%' + mf.field + '%')}/
11
+ result.gsub!(token, mf.value)
12
+ end
13
+ result
14
+ end
15
+
16
+ end
17
+
18
+ end
19
+
20
+ end
@@ -0,0 +1,3 @@
1
+ module Maildis
2
+ class ValidationError < StandardError; end
3
+ end
@@ -0,0 +1,19 @@
1
+ class ValidationUtils
2
+
3
+ class << self
4
+
5
+ def valid_hostname?(hostname)
6
+ return false unless hostname
7
+ return false if hostname.length > 255 || hostname.scan('..').any?
8
+ return true if hostname == 'localhost'
9
+ hostname = hostname[0 ... -1] if hostname.index('.', -1)
10
+ return hostname.split('.').collect { |i| i.size <= 63 && !(i.rindex('-', 0) || i.index('-', -1) || i.scan(/[^a-z\d-]/i).any?)}.all?
11
+ end
12
+
13
+ def valid_email?(email)
14
+ !email.match(/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i).nil?
15
+ end
16
+
17
+ end
18
+
19
+ end
@@ -0,0 +1,50 @@
1
+ require_relative 'validation_utils'
2
+ require_relative 'validation_error'
3
+ require_relative 'recipient_parser'
4
+
5
+ module Maildis
6
+
7
+ class Validator
8
+
9
+ class << self
10
+
11
+ def validate_config(hash)
12
+ raise ValidationError, "mailer settings missing" unless hash.has_key? 'mailer'
13
+ raise ValidationError, "smtp settings missing" unless hash.has_key? 'smtp'
14
+ end
15
+
16
+ def validate_subject(subject)
17
+ raise ValidationError, "mailer::subject invalid" unless !subject.nil? && !subject.empty?
18
+ end
19
+
20
+ def validate_smtp(hash)
21
+ raise ValidationError, "smtp::host missing or invalid" unless hash.has_key?('host') && ValidationUtils.valid_hostname?(hash['host'])
22
+ raise ValidationError, "smtp::port missing or invalid" unless hash.has_key?('port') && (hash['port'].to_i != 0)
23
+ raise ValidationError, "smtp::username missing" unless hash.has_key? 'username'
24
+ raise ValidationError, "smtp::password missing" unless hash.has_key? 'password'
25
+ end
26
+
27
+ def validate_sender(hash)
28
+ raise ValidationError, "sender::name missing" unless hash.has_key? 'name'
29
+ raise ValidationError, "sender::email missing or invalid" unless hash.has_key?('email') && ValidationUtils.valid_email?(hash['email'])
30
+ end
31
+
32
+ def validate_recipients(hash)
33
+ raise ValidationError, "recipients::csv missing" unless hash.has_key? 'csv'
34
+ raise ValidationError, "recipients::csv invalid file path" unless File.exist?(File.expand_path(hash['csv']))
35
+ raise ValidationError, "recipients::csv empty" if RecipientParser.empty_csv? hash['csv']
36
+ raise ValidationError, "recipients::csv invalid column headers" unless RecipientParser.valid_csv? hash['csv']
37
+ end
38
+
39
+ def validate_templates(hash)
40
+ raise ValidationError, "templates::html not specified" unless hash.has_key? 'html'
41
+ raise ValidationError, "templates::plain not specified" unless hash.has_key? 'plain'
42
+ raise ValidationError, "templates::html not found" unless File.exist?(File.expand_path(hash['html']))
43
+ raise ValidationError, "templates::plain not found" unless File.exist?(File.expand_path(hash['plain']))
44
+ end
45
+
46
+ end
47
+
48
+ end
49
+
50
+ end
@@ -0,0 +1,3 @@
1
+ module Maildis
2
+ VERSION = "0.0.1"
3
+ end