├── .gitignore ├── .gems ├── Makefile ├── test ├── helper.rb └── malone.rb ├── lib ├── malone │ └── test.rb └── malone.rb ├── malone.gemspec ├── CHANGELOG ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /pkg -------------------------------------------------------------------------------- /.gems: -------------------------------------------------------------------------------- 1 | cutest -v 1.2.2 2 | kuvert -v 0.0.1 3 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: .PHONY 2 | cutest test/*.rb 3 | 4 | .PHONY: 5 | -------------------------------------------------------------------------------- /test/helper.rb: -------------------------------------------------------------------------------- 1 | $:.unshift(File.expand_path("../lib", File.dirname(__FILE__))) 2 | 3 | require "cutest" 4 | require "malone" 5 | -------------------------------------------------------------------------------- /lib/malone/test.rb: -------------------------------------------------------------------------------- 1 | require "ostruct" 2 | 3 | class Malone 4 | def self.deliveries 5 | @deliveries ||= [] 6 | end 7 | 8 | def self.reset_deliveries 9 | @deliveries = nil 10 | end 11 | 12 | def deliver(*args) 13 | self.class.deliveries << OpenStruct.new(*args) 14 | end 15 | end 16 | 17 | 18 | -------------------------------------------------------------------------------- /malone.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'malone' 3 | s.version = "1.2.0" 4 | s.summary = %{The Mailman} 5 | s.description = %{Dead-simple Ruby mailing solution which always delivers.} 6 | s.date = "2011-01-10" 7 | s.author = "Cyril David" 8 | s.email = "cyx@cyx.is" 9 | s.homepage = "http://github.com/cyx/malone" 10 | s.files = `git ls-files`.split("\n") 11 | s.require_paths = ["lib"] 12 | 13 | s.add_dependency "kuvert", "~> 0.0" 14 | s.add_development_dependency "cutest", "~> 1.2" 15 | s.license = "MIT" 16 | end 17 | -------------------------------------------------------------------------------- /CHANGELOG: -------------------------------------------------------------------------------- 1 | 1.0.0 2 | 3 | - Malone.configure has been replaced with Malone.connect 4 | Use Malone.current to retrieve the last configured Malone 5 | instance (using Malone.connect) 6 | 7 | - Malone.deliver has been replaced with an instance method. 8 | Call deliver (with the same arguments as before) on the 9 | return value of Malone.connect or Malone.current. 10 | 11 | - Malone#deliver now receives :text and :html instead of :body. 12 | 13 | - No need to pass in TLS. it's tries to do TLS automatically. 14 | 15 | - Errors during SMTP authentication (and any other error) aren't 16 | trapped silently now. (via @tizoc, @elpollila) 17 | 18 | - Configuration parameters are more strict (i.e. passing :password 19 | would throw a NoMethodError via @tizoc, @elpollila) 20 | 21 | - malone/sandbox has been renamed to malone/test. 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2010 Cyril David 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | malone(1) -- send mail using smtp without any fuss 2 | ================================================== 3 | 4 | ## USAGE 5 | 6 | require "malone" 7 | 8 | # Typically you would do this somewhere in the bootstrapping 9 | # part of your application 10 | 11 | m = Malone.connect(url: "smtp://foo%40bar.com:pass@smtp.gmail.com:587", 12 | domain: "mysite.com") 13 | 14 | m.deliver(from: "me@me.com", to: "you@me.com", 15 | subject: "Test subject", text: "Great!") 16 | 17 | # Malone.current will now remember the last configuration you setup. 18 | Malone.current.config == m.config 19 | 20 | # Now you can also do Malone.deliver, which is syntactic sugar 21 | # for Malone.current.deliver 22 | Malone.deliver(from: "me@me.com", to: "you@me.com", 23 | subject: "Test subject", text: "Great!") 24 | 25 | # Also starting with Malone 1.0, you can pass in :html 26 | # for multipart emails. 27 | 28 | Malone.deliver(from: "me@me.com", to: "you@me.com", 29 | subject: "Test subject", 30 | text: "Great!", html: "Great!") 31 | 32 | 33 | ## TESTING 34 | 35 | require "malone/test" 36 | 37 | m = Malone.connect(url: "smtp://foo%40bar.com:pass@smtp.gmail.com:587", 38 | domain: "mysite.com") 39 | 40 | m.deliver(from: "me@me.com", to: "you@me.com", 41 | subject: "Test subject", text: "Great!") 42 | 43 | Malone.deliveries.size == 1 44 | # => true 45 | 46 | mail = Malone.deliveries.first 47 | 48 | "me@me.com" == mail.from 49 | # => true 50 | 51 | "you@me.com" == mail.to 52 | # => true 53 | 54 | "FooBar" == mail.text 55 | # => true 56 | 57 | "Hello World" == envelope.subject 58 | # => true 59 | 60 | ## INSTALLATION 61 | 62 | gem install malone 63 | 64 | ## CONFIGURATION TIPS 65 | 66 | If you're used to doing configuration via environment 67 | variables, similar to how Heroku does configuration, then 68 | you can simply set an environment variable in your 69 | production machine like so: 70 | 71 | export MALONE_URL=smtp://foo%40bar.com:pass@smtp.gmail.com:587 72 | 73 | Then you can connect using the environment variable in your 74 | code like so: 75 | 76 | Malone.connect(url: ENV["MALONE_URL"]) 77 | 78 | # or quite simply 79 | Malone.connect 80 | 81 | By default Malone tries for the environment variable `MALONE_URL` when 82 | you call `Malone.connect` without any arguments. 83 | -------------------------------------------------------------------------------- /lib/malone.rb: -------------------------------------------------------------------------------- 1 | require "cgi" 2 | require "kuvert" 3 | require "net/smtp" 4 | require "uri" 5 | 6 | class Malone 7 | attr :config 8 | 9 | def self.connect(options = {}) 10 | @config = Configuration.new(options) 11 | 12 | current 13 | end 14 | 15 | def self.current 16 | unless defined?(@config) 17 | raise RuntimeError, "Missing configuration: Try doing `Malone.connect`." 18 | end 19 | 20 | return new(@config) 21 | end 22 | 23 | def self.deliver(dict) 24 | current.deliver(dict) 25 | end 26 | 27 | def initialize(config) 28 | @config = config 29 | end 30 | 31 | def deliver(dict) 32 | mail = envelope(dict) 33 | yield mail if block_given? 34 | 35 | smtp = Net::SMTP.new(config.host, config.port) 36 | smtp.enable_starttls_auto if config.tls 37 | 38 | begin 39 | smtp.start(config.domain, config.user, config.password, config.auth) 40 | smtp.send_message(mail.to_s, mail.from.first, *mail.recipients) 41 | ensure 42 | smtp.finish if smtp.started? 43 | end 44 | end 45 | 46 | def envelope(dict) 47 | envelope = Kuvert.new 48 | envelope.from = dict[:from] 49 | envelope.to = dict[:to] 50 | envelope.replyto = dict[:replyto] 51 | envelope.cc = dict[:cc] if dict[:cc] 52 | envelope.bcc = dict[:bcc] if dict[:bcc] 53 | envelope.text = dict[:text] 54 | envelope.rawhtml = dict[:html] if dict[:html] 55 | envelope.subject = dict[:subject] 56 | 57 | envelope.attach(dict[:attach]) if dict[:attach] 58 | envelope.add_attachment_as(*dict[:attach_as]) if dict[:attach_as] 59 | 60 | return envelope 61 | end 62 | 63 | class Configuration 64 | attr_accessor :host 65 | attr_accessor :port 66 | attr_accessor :user 67 | attr_accessor :password 68 | attr_accessor :domain 69 | attr_accessor :tls 70 | 71 | attr :auth 72 | 73 | def initialize(options) 74 | opts = options.dup 75 | 76 | @tls = true 77 | 78 | url = opts.delete(:url) || ENV["MALONE_URL"] 79 | 80 | if url 81 | uri = URI(url) 82 | 83 | opts[:host] ||= uri.host 84 | opts[:port] ||= uri.port.to_i 85 | opts[:user] ||= unescaped(uri.user) 86 | opts[:password] ||= unescaped(uri.password) 87 | end 88 | 89 | opts.each do |key, val| 90 | send(:"#{key}=", val) 91 | end 92 | end 93 | 94 | def auth=(val) 95 | @auth = val && val.to_sym 96 | end 97 | 98 | private 99 | def unescaped(val) 100 | return if val.to_s.empty? 101 | 102 | CGI.unescape(val) 103 | end 104 | end 105 | end 106 | -------------------------------------------------------------------------------- /test/malone.rb: -------------------------------------------------------------------------------- 1 | require_relative "helper" 2 | 3 | test "basic configuration" do 4 | m = Malone.connect(host: "smtp.gmail.com", port: 587, 5 | user: "foo@bar.com", password: "pass1234", 6 | domain: "foo.com", auth: "login") 7 | 8 | c = m.config 9 | 10 | assert_equal "smtp.gmail.com", c.host 11 | assert_equal 587, c.port 12 | assert_equal "foo@bar.com", c.user 13 | assert_equal "pass1234", c.password 14 | assert_equal "foo.com", c.domain 15 | assert_equal :login, c.auth 16 | end 17 | 18 | test "configuration via url" do 19 | m = Malone.connect(url: "smtp://foo%40bar.com:pass1234@smtp.gmail.com:587") 20 | 21 | c = m.config 22 | 23 | assert_equal "smtp.gmail.com", c.host 24 | assert_equal 587, c.port 25 | assert_equal "foo@bar.com", c.user 26 | assert_equal "pass1234", c.password 27 | end 28 | 29 | test "configuration via url and params" do 30 | m = Malone.connect(url: "smtp://foo%40bar.com:pass1234@smtp.gmail.com:587", 31 | domain: "foo.com", auth: "login", password: "barbaz123") 32 | 33 | c = m.config 34 | 35 | assert_equal "smtp.gmail.com", c.host 36 | assert_equal 587, c.port 37 | assert_equal "foo@bar.com", c.user 38 | assert_equal "foo.com", c.domain 39 | assert_equal :login, c.auth 40 | assert_equal true, c.tls 41 | 42 | # We verify that parameters passed takes precedence over the URL. 43 | assert_equal "barbaz123", c.password 44 | end 45 | 46 | test "configuration via MALONE_URL" do 47 | ENV["MALONE_URL"] = "smtp://foo%40bar.com:pass1234@smtp.gmail.com:587" 48 | 49 | m = Malone.connect(domain: "foo.com", auth: "login", tls: false) 50 | c = m.config 51 | 52 | assert_equal "smtp.gmail.com", c.host 53 | assert_equal 587, c.port 54 | assert_equal "foo@bar.com", c.user 55 | assert_equal "foo.com", c.domain 56 | assert_equal :login, c.auth 57 | assert_equal false, c.tls 58 | end 59 | 60 | test "typos in configuration" do 61 | assert_raise NoMethodError do 62 | Malone.connect(pass: "pass") 63 | end 64 | end 65 | 66 | test "Malone.connect doesn't mutate the options" do 67 | ex = nil 68 | begin 69 | Malone.connect({}.freeze) 70 | rescue RuntimeError => ex 71 | end 72 | 73 | assert_equal nil, ex 74 | end 75 | 76 | test "Malone.current" do 77 | Malone.connect(url: "smtp://foo%40bar.com:pass1234@smtp.gmail.com:587") 78 | 79 | c = Malone.current.config 80 | 81 | assert_equal "smtp.gmail.com", c.host 82 | assert_equal 587, c.port 83 | assert_equal "foo@bar.com", c.user 84 | assert_equal "pass1234", c.password 85 | end 86 | 87 | test "#envelope" do 88 | m = Malone.connect 89 | 90 | mail = m.envelope(to: "recipient@me.com", from: "no-reply@mydomain.com", 91 | subject: "SUB", text: "TEXT", html: "