├── bin ├── setup └── console ├── .gitignore ├── LICENSE.txt ├── .github ├── dependabot.yml └── workflows │ └── test.yml ├── Gemfile ├── Rakefile ├── lib └── net │ ├── smtp │ ├── auth_plain.rb │ ├── auth_login.rb │ ├── auth_xoauth2.rb │ ├── auth_cram_md5.rb │ └── authenticator.rb │ └── smtp.rb ├── test └── net │ ├── fixtures │ ├── Makefile │ ├── server.crt │ ├── cacert.pem │ ├── dhparams.pem │ └── server.key │ └── smtp │ ├── test_response.rb │ ├── test_starttls.rb │ ├── test_sslcontext.rb │ └── test_smtp.rb ├── net-smtp.gemspec ├── BSDL ├── COPYING ├── README.md └── NEWS.md /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "net/smtp" 5 | 6 | require "irb" 7 | IRB.start(__FILE__) 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /_yardoc/ 4 | /coverage/ 5 | /doc/ 6 | /pkg/ 7 | /spec/reports/ 8 | /tmp/ 9 | Gemfile.lock 10 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | All the files in this distribution are covered under either the Ruby license or 2 | the BSD-2-Clause license (see the file COPYING). 3 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: 'github-actions' 4 | directory: '/' 5 | schedule: 6 | interval: 'weekly' 7 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "https://rubygems.org" 2 | 3 | gemspec 4 | 5 | group :development do 6 | gem "bundler" 7 | gem "rake" 8 | gem "test-unit" 9 | end 10 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | require "rake/testtask" 3 | 4 | Rake::TestTask.new(:test) do |t| 5 | t.libs << "test" 6 | t.libs << "lib" 7 | t.test_files = FileList['test/**/test_*.rb'] 8 | end 9 | 10 | task :default => [:test] 11 | -------------------------------------------------------------------------------- /lib/net/smtp/auth_plain.rb: -------------------------------------------------------------------------------- 1 | class Net::SMTP 2 | class AuthPlain < Net::SMTP::Authenticator 3 | auth_type :plain 4 | 5 | def auth(user, secret) 6 | finish('AUTH PLAIN ' + base64_encode("\0#{user}\0#{secret}")) 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /lib/net/smtp/auth_login.rb: -------------------------------------------------------------------------------- 1 | class Net::SMTP 2 | class AuthLogin < Net::SMTP::Authenticator 3 | auth_type :login 4 | 5 | def auth(user, secret) 6 | continue('AUTH LOGIN') 7 | continue(base64_encode(user)) 8 | finish(base64_encode(secret)) 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /lib/net/smtp/auth_xoauth2.rb: -------------------------------------------------------------------------------- 1 | class Net::SMTP 2 | class AuthXoauth2 < Net::SMTP::Authenticator 3 | auth_type :xoauth2 4 | 5 | def auth(user, secret) 6 | token = xoauth2_string(user, secret) 7 | 8 | finish("AUTH XOAUTH2 #{base64_encode(token)}") 9 | end 10 | 11 | private 12 | 13 | def xoauth2_string(user, secret) 14 | "user=#{user}\1auth=Bearer #{secret}\1\1" 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /test/net/fixtures/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | 3 | regen_certs: 4 | touch server.key 5 | make server.crt 6 | 7 | cacert.pem: server.key 8 | openssl req -new -x509 -days 3650 -key server.key -out cacert.pem -subj "/C=JP/ST=Shimane/L=Matz-e city/O=Ruby Core Team/CN=Ruby Test CA/emailAddress=security@ruby-lang.org" 9 | 10 | server.csr: 11 | openssl req -new -key server.key -out server.csr -subj "/C=JP/ST=Shimane/O=Ruby Core Team/OU=Ruby Test/CN=localhost" 12 | 13 | server.crt: server.csr cacert.pem 14 | openssl x509 -days 3650 -CA cacert.pem -CAkey server.key -set_serial 00 -in server.csr -req -out server.crt 15 | rm server.csr 16 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | ruby-versions: 7 | uses: ruby/actions/.github/workflows/ruby_versions.yml@master 8 | with: 9 | engine: cruby 10 | min_version: 2.6 11 | 12 | build: 13 | needs: ruby-versions 14 | name: build (${{ matrix.ruby }} / ${{ matrix.os }}) 15 | strategy: 16 | matrix: 17 | ruby: ${{ fromJson(needs.ruby-versions.outputs.versions) }} 18 | os: [ ubuntu-latest, macos-latest, windows-latest ] 19 | exclude: 20 | - ruby: 2.6 21 | os: windows-latest 22 | - ruby: 2.7 23 | os: windows-latest 24 | runs-on: ${{ matrix.os }} 25 | steps: 26 | - uses: actions/checkout@v6 27 | - name: Set up Ruby 28 | uses: ruby/setup-ruby@v1 29 | with: 30 | ruby-version: ${{ matrix.ruby }} 31 | - name: Install dependencies 32 | run: bundle install 33 | - name: Run test 34 | run: rake test 35 | -------------------------------------------------------------------------------- /net-smtp.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | name = File.basename(__FILE__, ".gemspec") 4 | version = ["lib", Array.new(name.count("-"), "..").join("/")].find do |dir| 5 | break File.foreach(File.join(__dir__, dir, "#{name.tr('-', '/')}.rb"), :encoding => "UTF-8") do |line| 6 | /^\s*VERSION\s*=\s*"(.*)"/ =~ line and break $1 7 | end rescue nil 8 | end 9 | 10 | Gem::Specification.new do |spec| 11 | spec.name = name 12 | spec.version = version 13 | spec.authors = ["Yukihiro Matsumoto"] 14 | spec.email = ["matz@ruby-lang.org"] 15 | 16 | spec.summary = %q{Simple Mail Transfer Protocol client library for Ruby.} 17 | spec.description = %q{Simple Mail Transfer Protocol client library for Ruby.} 18 | spec.homepage = "https://github.com/ruby/net-smtp" 19 | spec.licenses = ["Ruby", "BSD-2-Clause"] 20 | spec.required_ruby_version = ">= 2.6.0" 21 | 22 | spec.metadata["homepage_uri"] = spec.homepage 23 | spec.metadata["source_code_uri"] = spec.homepage 24 | 25 | spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do 26 | `git ls-files README.md NEWS.md LICENSE.txt net-smtp.gemspec lib`.split 27 | end 28 | spec.require_paths = ["lib"] 29 | 30 | spec.add_dependency "net-protocol" 31 | end 32 | -------------------------------------------------------------------------------- /test/net/fixtures/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDYTCCAkkCAQAwDQYJKoZIhvcNAQELBQAwgYwxCzAJBgNVBAYTAkpQMRAwDgYD 3 | VQQIDAdTaGltYW5lMRQwEgYDVQQHDAtNYXR6LWUgY2l0eTEXMBUGA1UECgwOUnVi 4 | eSBDb3JlIFRlYW0xFTATBgNVBAMMDFJ1YnkgVGVzdCBDQTElMCMGCSqGSIb3DQEJ 5 | ARYWc2VjdXJpdHlAcnVieS1sYW5nLm9yZzAeFw0yNDAxMDExMTQ3MjNaFw0zMzEy 6 | MjkxMTQ3MjNaMGAxCzAJBgNVBAYTAkpQMRAwDgYDVQQIDAdTaGltYW5lMRcwFQYD 7 | VQQKDA5SdWJ5IENvcmUgVGVhbTESMBAGA1UECwwJUnVieSBUZXN0MRIwEAYDVQQD 8 | DAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCw+egZ 9 | Q6eumJKq3hfKfED4dE/tL4FI5sjqont9ABVI+1GSqyi1bFBgsRjM0THllIdMbKmJ 10 | tWwnKW8J+5OgNN8y6Xxv8JmM/Y5vQt2lis0fqXmG8UTz0VTWdlAXXmhUs6lSADvA 11 | aIe4RVrCsZ97L3ZQTryY7JRVcbB4khUN3Gp0yg+801SXzoFTTa+UGIRLE66jH51a 12 | a5VXu99hnv1OiH8tQrjdi8mH6uG/icq4XuIeNWMF32wHqIOOPvQcWV3M5D2vxJEj 13 | 702Ku6k9OQXkAo17qRSEonWW4HtLbtmS8He1JNPc/n3dVUm+fM6NoDXPoLP7j55G 14 | 9zKyqGtGAWXAj1MTAgMBAAEwDQYJKoZIhvcNAQELBQADggEBACtGNdj5TEtnJBYp 15 | M+LhBeU3oNteldfycEm993gJp6ghWZFg23oX8fVmyEeJr/3Ca9bAgDqg0t9a0npN 16 | oWKEY6wVKqcHgu3gSvThF5c9KhGbeDDmlTSVVNQmXWX0K2d4lS2cwZHH8mCm2mrY 17 | PDqlEkSc7k4qSiqigdS8i80Yk+lDXWsm8CjsiC93qaRM7DnS0WPQR0c16S95oM6G 18 | VklFKUSDAuFjw9aVWA/nahOucjn0w5fVW6lyIlkBslC1ChlaDgJmvhz+Ol3iMsE0 19 | kAmFNu2KKPVrpMWaBID49QwQTDyhetNLaVVFM88iUdA9JDoVMEuP1mm39JqyzHTu 20 | uBrdP4Q= 21 | -----END CERTIFICATE----- 22 | -------------------------------------------------------------------------------- /BSDL: -------------------------------------------------------------------------------- 1 | Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions 5 | are met: 6 | 1. Redistributions of source code must retain the above copyright 7 | notice, this list of conditions and the following disclaimer. 8 | 2. Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND 13 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 14 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 15 | ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE 16 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 17 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS 18 | OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) 19 | HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT 20 | LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY 21 | OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF 22 | SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /lib/net/smtp/auth_cram_md5.rb: -------------------------------------------------------------------------------- 1 | unless defined? OpenSSL 2 | begin 3 | require 'digest/md5' 4 | rescue LoadError 5 | end 6 | end 7 | 8 | class Net::SMTP 9 | class AuthCramMD5 < Net::SMTP::Authenticator 10 | auth_type :cram_md5 11 | 12 | def auth(user, secret) 13 | challenge = continue('AUTH CRAM-MD5') 14 | crammed = cram_md5_response(secret, challenge.unpack1('m')) 15 | finish(base64_encode("#{user} #{crammed}")) 16 | end 17 | 18 | IMASK = 0x36 19 | OMASK = 0x5c 20 | 21 | # CRAM-MD5: [RFC2195] 22 | def cram_md5_response(secret, challenge) 23 | tmp = digest_class::MD5.digest(cram_secret(secret, IMASK) + challenge) 24 | digest_class::MD5.hexdigest(cram_secret(secret, OMASK) + tmp) 25 | end 26 | 27 | CRAM_BUFSIZE = 64 28 | 29 | def cram_secret(secret, mask) 30 | secret = digest_class::MD5.digest(secret) if secret.size > CRAM_BUFSIZE 31 | buf = secret.ljust(CRAM_BUFSIZE, "\0") 32 | 0.upto(buf.size - 1) do |i| 33 | buf[i] = (buf[i].ord ^ mask).chr 34 | end 35 | buf 36 | end 37 | 38 | def digest_class 39 | @digest_class ||= if defined?(OpenSSL::Digest) 40 | OpenSSL::Digest 41 | elsif defined?(::Digest) 42 | ::Digest 43 | else 44 | raise '"openssl" or "digest" library is required' 45 | end 46 | end 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /test/net/fixtures/cacert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIID+zCCAuOgAwIBAgIUGMvHl3EhtKPKcgc3NQSAYfFuC+8wDQYJKoZIhvcNAQEL 3 | BQAwgYwxCzAJBgNVBAYTAkpQMRAwDgYDVQQIDAdTaGltYW5lMRQwEgYDVQQHDAtN 4 | YXR6LWUgY2l0eTEXMBUGA1UECgwOUnVieSBDb3JlIFRlYW0xFTATBgNVBAMMDFJ1 5 | YnkgVGVzdCBDQTElMCMGCSqGSIb3DQEJARYWc2VjdXJpdHlAcnVieS1sYW5nLm9y 6 | ZzAeFw0yNDAxMDExMTQ3MjNaFw0zMzEyMjkxMTQ3MjNaMIGMMQswCQYDVQQGEwJK 7 | UDEQMA4GA1UECAwHU2hpbWFuZTEUMBIGA1UEBwwLTWF0ei1lIGNpdHkxFzAVBgNV 8 | BAoMDlJ1YnkgQ29yZSBUZWFtMRUwEwYDVQQDDAxSdWJ5IFRlc3QgQ0ExJTAjBgkq 9 | hkiG9w0BCQEWFnNlY3VyaXR5QHJ1YnktbGFuZy5vcmcwggEiMA0GCSqGSIb3DQEB 10 | AQUAA4IBDwAwggEKAoIBAQCw+egZQ6eumJKq3hfKfED4dE/tL4FI5sjqont9ABVI 11 | +1GSqyi1bFBgsRjM0THllIdMbKmJtWwnKW8J+5OgNN8y6Xxv8JmM/Y5vQt2lis0f 12 | qXmG8UTz0VTWdlAXXmhUs6lSADvAaIe4RVrCsZ97L3ZQTryY7JRVcbB4khUN3Gp0 13 | yg+801SXzoFTTa+UGIRLE66jH51aa5VXu99hnv1OiH8tQrjdi8mH6uG/icq4XuIe 14 | NWMF32wHqIOOPvQcWV3M5D2vxJEj702Ku6k9OQXkAo17qRSEonWW4HtLbtmS8He1 15 | JNPc/n3dVUm+fM6NoDXPoLP7j55G9zKyqGtGAWXAj1MTAgMBAAGjUzBRMB0GA1Ud 16 | DgQWBBSJGVleDvFp9cu9R+E0/OKYzGkwkTAfBgNVHSMEGDAWgBSJGVleDvFp9cu9 17 | R+E0/OKYzGkwkTAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBl 18 | 8GLB8skAWlkSw/FwbUmEV3zyqu+p7PNP5YIYoZs0D74e7yVulGQ6PKMZH5hrZmHo 19 | orFSQU+VUUirG8nDGj7Rzce8WeWBxsaDGC8CE2dq6nC6LuUwtbdMnBrH0LRWAz48 20 | jGFF3jHtVz8VsGfoZTZCjukWqNXvU6hETT9GsfU+PZqbqcTVRPH52+XgYayKdIbD 21 | r97RM4X3+aXBHcUW0b76eyyi65RR/Xtvn8ioZt2AdX7T2tZzJyXJN3Hupp77s6Ui 22 | AZR35SToHCZeTZD12YBvLBdaTPLZN7O/Q/aAO9ZiJaZ7SbFOjz813B2hxXab4Fob 23 | 2uJX6eMWTVxYK5D4M9lm 24 | -----END CERTIFICATE----- 25 | -------------------------------------------------------------------------------- /test/net/fixtures/dhparams.pem: -------------------------------------------------------------------------------- 1 | DH Parameters: (2048 bit) 2 | prime: 3 | 00:ec:4e:a4:06:b6:22:ca:f9:8a:00:cc:d0:ee:2f: 4 | 16:bf:05:64:f5:8f:fe:7f:c4:bb:b0:24:cd:ef:5d: 5 | 8a:90:ad:dc:a9:dd:63:84:90:d8:25:ba:d8:78:d5: 6 | 77:91:42:0a:84:fc:56:1e:13:9b:1c:aa:43:d5:1f: 7 | 38:52:92:fe:b3:66:f9:e7:e8:8c:77:a1:a6:2f:b3: 8 | 98:98:d2:13:fc:57:1c:2a:14:dc:bd:e6:9b:54:19: 9 | 99:4f:ce:81:64:a6:32:7f:8e:61:50:5f:45:3a:e5: 10 | 0c:f7:13:f3:b8:ad:d5:77:ca:09:42:f7:d8:30:27: 11 | 7b:2c:f0:b4:b5:a0:04:96:34:0b:47:81:1d:7f:c1: 12 | 3a:62:86:8e:7d:f8:13:7f:9a:b1:8b:09:23:9e:55: 13 | 59:41:cd:f0:86:09:c4:b7:d1:69:54:cb:d0:f5:e9: 14 | 27:c9:e1:81:e4:a1:df:6b:20:1c:df:e8:54:02:f2: 15 | 37:fc:2a:f7:d5:b3:6f:79:7e:70:22:78:79:18:3c: 16 | 75:14:68:4a:05:9f:ac:d4:7f:9a:79:db:9d:0a:6e: 17 | ec:0a:04:70:bf:c9:4a:59:81:a2:1f:33:9b:4a:66: 18 | bc:03:ce:8a:1b:e3:03:ec:ba:39:26:ab:90:dc:39: 19 | 41:a1:d8:f7:20:3c:8f:af:12:2f:f7:a9:6f:44:f1: 20 | 6d:03 21 | generator: 2 (0x2) 22 | -----BEGIN DH PARAMETERS----- 23 | MIIBCAKCAQEA7E6kBrYiyvmKAMzQ7i8WvwVk9Y/+f8S7sCTN712KkK3cqd1jhJDY 24 | JbrYeNV3kUIKhPxWHhObHKpD1R84UpL+s2b55+iMd6GmL7OYmNIT/FccKhTcveab 25 | VBmZT86BZKYyf45hUF9FOuUM9xPzuK3Vd8oJQvfYMCd7LPC0taAEljQLR4Edf8E6 26 | YoaOffgTf5qxiwkjnlVZQc3whgnEt9FpVMvQ9eknyeGB5KHfayAc3+hUAvI3/Cr3 27 | 1bNveX5wInh5GDx1FGhKBZ+s1H+aedudCm7sCgRwv8lKWYGiHzObSma8A86KG+MD 28 | 7Lo5JquQ3DlBodj3IDyPrxIv96lvRPFtAwIBAg== 29 | -----END DH PARAMETERS----- 30 | -------------------------------------------------------------------------------- /lib/net/smtp/authenticator.rb: -------------------------------------------------------------------------------- 1 | module Net 2 | class SMTP 3 | class Authenticator 4 | def self.auth_classes 5 | @classes ||= {} 6 | end 7 | 8 | def self.auth_type(type) 9 | type = type.to_s.upcase.tr(?_, ?-).to_sym 10 | Authenticator.auth_classes[type] = self 11 | end 12 | 13 | def self.auth_class(type) 14 | type = type.to_s.upcase.tr(?_, ?-).to_sym 15 | Authenticator.auth_classes[type] 16 | end 17 | 18 | def self.check_args(user_arg = nil, secret_arg = nil, *, **) 19 | unless user_arg 20 | raise ArgumentError, 'SMTP-AUTH requested but missing user name' 21 | end 22 | unless secret_arg 23 | raise ArgumentError, 'SMTP-AUTH requested but missing secret phrase' 24 | end 25 | end 26 | 27 | attr_reader :smtp 28 | 29 | def initialize(smtp) 30 | @smtp = smtp 31 | end 32 | 33 | # @param arg [String] message to server 34 | # @return [String] message from server 35 | def continue(arg) 36 | res = smtp.get_response arg 37 | raise res.exception_class.new(res) unless res.continue? 38 | res.string.split[1] 39 | end 40 | 41 | # @param arg [String] message to server 42 | # @return [Net::SMTP::Response] response from server 43 | def finish(arg) 44 | res = smtp.get_response arg 45 | raise SMTPAuthenticationError.new(res) unless res.success? 46 | res 47 | end 48 | 49 | # @param str [String] 50 | # @return [String] Base64 encoded string 51 | def base64_encode(str) 52 | # expects "str" may not become too long 53 | [str].pack('m0') 54 | end 55 | end 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /test/net/fixtures/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEowIBAAKCAQEAsPnoGUOnrpiSqt4XynxA+HRP7S+BSObI6qJ7fQAVSPtRkqso 3 | tWxQYLEYzNEx5ZSHTGypibVsJylvCfuToDTfMul8b/CZjP2Ob0LdpYrNH6l5hvFE 4 | 89FU1nZQF15oVLOpUgA7wGiHuEVawrGfey92UE68mOyUVXGweJIVDdxqdMoPvNNU 5 | l86BU02vlBiESxOuox+dWmuVV7vfYZ79Toh/LUK43YvJh+rhv4nKuF7iHjVjBd9s 6 | B6iDjj70HFldzOQ9r8SRI+9NirupPTkF5AKNe6kUhKJ1luB7S27ZkvB3tSTT3P59 7 | 3VVJvnzOjaA1z6Cz+4+eRvcysqhrRgFlwI9TEwIDAQABAoIBAEEYiyDP29vCzx/+ 8 | dS3LqnI5BjUuJhXUnc6AWX/PCgVAO+8A+gZRgvct7PtZb0sM6P9ZcLrweomlGezI 9 | FrL0/6xQaa8bBr/ve/a8155OgcjFo6fZEw3Dz7ra5fbSiPmu4/b/kvrg+Br1l77J 10 | aun6uUAs1f5B9wW+vbR7tzbT/mxaUeDiBzKpe15GwcvbJtdIVMa2YErtRjc1/5B2 11 | BGVXyvlJv0SIlcIEMsHgnAFOp1ZgQ08aDzvilLq8XVMOahAhP1O2A3X8hKdXPyrx 12 | IVWE9bS9ptTo+eF6eNl+d7htpKGEZHUxinoQpWEBTv+iOoHsVunkEJ3vjLP3lyI/ 13 | fY0NQ1ECgYEA3RBXAjgvIys2gfU3keImF8e/TprLge1I2vbWmV2j6rZCg5r/AS0u 14 | pii5CvJ5/T5vfJPNgPBy8B/yRDs+6PJO1GmnlhOkG9JAIPkv0RBZvR0PMBtbp6nT 15 | Y3yo1lwamBVBfY6rc0sLTzosZh2aGoLzrHNMQFMGaauORzBFpY5lU50CgYEAzPHl 16 | u5DI6Xgep1vr8QvCUuEesCOgJg8Yh1UqVoY/SmQh6MYAv1I9bLGwrb3WW/7kqIoD 17 | fj0aQV5buVZI2loMomtU9KY5SFIsPV+JuUpy7/+VE01ZQM5FdY8wiYCQiVZYju9X 18 | Wz5LxMNoz+gT7pwlLCsC4N+R8aoBk404aF1gum8CgYAJ7VTq7Zj4TFV7Soa/T1eE 19 | k9y8a+kdoYk3BASpCHJ29M5R2KEA7YV9wrBklHTz8VzSTFTbKHEQ5W5csAhoL5Fo 20 | qoHzFFi3Qx7MHESQb9qHyolHEMNx6QdsHUn7rlEnaTTyrXh3ifQtD6C0yTmFXUIS 21 | CW9wKApOrnyKJ9nI0HcuZQKBgQCMtoV6e9VGX4AEfpuHvAAnMYQFgeBiYTkBKltQ 22 | XwozhH63uMMomUmtSG87Sz1TmrXadjAhy8gsG6I0pWaN7QgBuFnzQ/HOkwTm+qKw 23 | AsrZt4zeXNwsH7QXHEJCFnCmqw9QzEoZTrNtHJHpNboBuVnYcoueZEJrP8OnUG3r 24 | UjmopwKBgAqB2KYYMUqAOvYcBnEfLDmyZv9BTVNHbR2lKkMYqv5LlvDaBxVfilE0 25 | 2riO4p6BaAdvzXjKeRrGNEKoHNBpOSfYCOM16NjL8hIZB1CaV3WbT5oY+jp7Mzd5 26 | 7d56RZOE+ERK2uz/7JX9VSsM/LbH9pJibd4e8mikDS9ntciqOH/3 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | Ruby is copyrighted free software by Yukihiro Matsumoto . 2 | You can redistribute it and/or modify it under either the terms of the 3 | 2-clause BSDL (see the file BSDL), or the conditions below: 4 | 5 | 1. You may make and give away verbatim copies of the source form of the 6 | software without restriction, provided that you duplicate all of the 7 | original copyright notices and associated disclaimers. 8 | 9 | 2. You may modify your copy of the software in any way, provided that 10 | you do at least ONE of the following: 11 | 12 | a. place your modifications in the Public Domain or otherwise 13 | make them Freely Available, such as by posting said 14 | modifications to Usenet or an equivalent medium, or by allowing 15 | the author to include your modifications in the software. 16 | 17 | b. use the modified software only within your corporation or 18 | organization. 19 | 20 | c. give non-standard binaries non-standard names, with 21 | instructions on where to get the original software distribution. 22 | 23 | d. make other distribution arrangements with the author. 24 | 25 | 3. You may distribute the software in object code or binary form, 26 | provided that you do at least ONE of the following: 27 | 28 | a. distribute the binaries and library files of the software, 29 | together with instructions (in the manual page or equivalent) 30 | on where to get the original distribution. 31 | 32 | b. accompany the distribution with the machine-readable source of 33 | the software. 34 | 35 | c. give non-standard binaries non-standard names, with 36 | instructions on where to get the original software distribution. 37 | 38 | d. make other distribution arrangements with the author. 39 | 40 | 4. You may modify and include the part of the software into any other 41 | software (possibly commercial). But some files in the distribution 42 | are not written by the author, so that they are not under these terms. 43 | 44 | For the list of those files and their copying conditions, see the 45 | file LEGAL. 46 | 47 | 5. The scripts and library files supplied as input to or produced as 48 | output from the software do not automatically fall under the 49 | copyright of the software, but belong to whomever generated them, 50 | and may be sold commercially, and may be aggregated with this 51 | software. 52 | 53 | 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR 54 | IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED 55 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 56 | PURPOSE. 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Net::SMTP 2 | 3 | This library provides functionality to send internet mail via SMTP, the Simple Mail Transfer Protocol. 4 | 5 | For details of SMTP itself, see [RFC2821](http://www.ietf.org/rfc/rfc2821.txt). 6 | 7 | ## Installation 8 | 9 | Add this line to your application's Gemfile: 10 | 11 | ```ruby 12 | gem 'net-smtp' 13 | ``` 14 | 15 | And then execute: 16 | 17 | $ bundle 18 | 19 | Or install it yourself as: 20 | 21 | $ gem install net-smtp 22 | 23 | ## Usage 24 | 25 | ### Sending Messages 26 | 27 | You must open a connection to an SMTP server before sending messages. 28 | The first argument is the address of your SMTP server, and the second 29 | argument is the port number. Using SMTP.start with a block is the simplest 30 | way to do this. This way, the SMTP connection is closed automatically 31 | after the block is executed. 32 | 33 | ```ruby 34 | require 'net/smtp' 35 | Net::SMTP.start('your.smtp.server', 25) do |smtp| 36 | # Use the SMTP object smtp only in this block. 37 | end 38 | ``` 39 | 40 | Replace 'your.smtp.server' with your SMTP server. Normally 41 | your system manager or internet provider supplies a server 42 | for you. 43 | 44 | Then you can send messages. 45 | 46 | ```ruby 47 | msgstr = < 49 | To: Destination Address 50 | Subject: test message 51 | Date: Sat, 23 Jun 2001 16:26:43 +0900 52 | Message-Id: 53 | 54 | This is a test message. 55 | END_OF_MESSAGE 56 | 57 | require 'net/smtp' 58 | Net::SMTP.start('your.smtp.server', 25) do |smtp| 59 | smtp.send_message msgstr, 60 | 'your@mail.address', 61 | 'his_address@example.com' 62 | end 63 | ``` 64 | 65 | ### Closing the Session 66 | 67 | You MUST close the SMTP session after sending messages, by calling 68 | the #finish method: 69 | 70 | ```ruby 71 | # using SMTP#finish 72 | smtp = Net::SMTP.start('your.smtp.server', 25) 73 | smtp.send_message msgstr, 'from@address', 'to@address' 74 | smtp.finish 75 | ``` 76 | 77 | You can also use the block form of SMTP.start/SMTP#start. This closes 78 | the SMTP session automatically: 79 | 80 | ```ruby 81 | # using block form of SMTP.start 82 | Net::SMTP.start('your.smtp.server', 25) do |smtp| 83 | smtp.send_message msgstr, 'from@address', 'to@address' 84 | end 85 | ``` 86 | 87 | I strongly recommend this scheme. This form is simpler and more robust. 88 | 89 | ## Development 90 | 91 | After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment. 92 | 93 | To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org). 94 | 95 | ## Contributing 96 | 97 | Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/net-smtp. 98 | -------------------------------------------------------------------------------- /test/net/smtp/test_response.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | require 'net/smtp' 3 | require 'test/unit' 4 | 5 | module Net 6 | class SMTP 7 | class TestResponse < Test::Unit::TestCase 8 | def test_capabilities 9 | res = Response.parse("250-ubuntu-desktop\n250-PIPELINING\n250-SIZE 10240000\n250-VRFY\n250-ETRN\n250-STARTTLS\n250-ENHANCEDSTATUSCODES\n250 DSN\n") 10 | 11 | capabilities = res.capabilities 12 | %w{ PIPELINING SIZE VRFY STARTTLS ENHANCEDSTATUSCODES DSN}.each do |str| 13 | assert capabilities.key?(str), str 14 | end 15 | end 16 | 17 | def test_capabilities_default 18 | res = Response.parse("250-ubuntu-desktop\n250-PIPELINING\n250 DSN\n") 19 | assert_equal [], res.capabilities['PIPELINING'] 20 | end 21 | 22 | def test_capabilities_value 23 | res = Response.parse("250-ubuntu-desktop\n250-SIZE 1234\n250 DSN\n") 24 | assert_equal ['1234'], res.capabilities['SIZE'] 25 | end 26 | 27 | def test_capabilities_multi 28 | res = Response.parse("250-ubuntu-desktop\n250-SIZE 1 2 3\n250 DSN\n") 29 | assert_equal %w{1 2 3}, res.capabilities['SIZE'] 30 | end 31 | 32 | def test_bad_string 33 | res = Response.parse("badstring") 34 | assert_equal({}, res.capabilities) 35 | end 36 | 37 | def test_success? 38 | res = Response.parse("250-ubuntu-desktop\n250-SIZE 1 2 3\n250 DSN\n") 39 | assert res.success? 40 | assert !res.continue? 41 | end 42 | 43 | # RFC 2821, Section 4.2.1 44 | def test_continue? 45 | res = Response.parse("3yz-ubuntu-desktop\n250-SIZE 1 2 3\n250 DSN\n") 46 | assert !res.success? 47 | assert res.continue? 48 | end 49 | 50 | def test_status_type_char 51 | res = Response.parse("3yz-ubuntu-desktop\n250-SIZE 1 2 3\n250 DSN\n") 52 | assert_equal '3', res.status_type_char 53 | 54 | res = Response.parse("250-ubuntu-desktop\n250-SIZE 1 2 3\n250 DSN\n") 55 | assert_equal '2', res.status_type_char 56 | end 57 | 58 | def test_message 59 | res = Response.parse("250-ubuntu-desktop\n250-SIZE 1 2 3\n250 DSN\n") 60 | assert_equal "250-ubuntu-desktop\n", res.message 61 | end 62 | 63 | def test_server_busy_exception 64 | res = Response.parse("400 omg busy") 65 | assert_equal Net::SMTPServerBusy, res.exception_class 66 | res = Response.parse("410 omg busy") 67 | assert_equal Net::SMTPServerBusy, res.exception_class 68 | end 69 | 70 | def test_syntax_error_exception 71 | res = Response.parse("500 omg syntax error") 72 | assert_equal Net::SMTPSyntaxError, res.exception_class 73 | 74 | res = Response.parse("501 omg syntax error") 75 | assert_equal Net::SMTPSyntaxError, res.exception_class 76 | end 77 | 78 | def test_authentication_exception 79 | res = Response.parse("530 omg auth error") 80 | assert_equal Net::SMTPAuthenticationError, res.exception_class 81 | 82 | res = Response.parse("531 omg auth error") 83 | assert_equal Net::SMTPAuthenticationError, res.exception_class 84 | end 85 | 86 | def test_fatal_error 87 | res = Response.parse("510 omg fatal error") 88 | assert_equal Net::SMTPFatalError, res.exception_class 89 | 90 | res = Response.parse("511 omg fatal error") 91 | assert_equal Net::SMTPFatalError, res.exception_class 92 | end 93 | 94 | def test_default_exception 95 | res = Response.parse("250 omg fatal error") 96 | assert_equal Net::SMTPUnknownError, res.exception_class 97 | end 98 | end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /test/net/smtp/test_starttls.rb: -------------------------------------------------------------------------------- 1 | require 'net/smtp' 2 | require 'test/unit' 3 | 4 | unless defined?(OpenSSL::VERSION) 5 | warn "#{__FILE__}: openssl library not loaded; skipping" 6 | return 7 | end 8 | 9 | module Net 10 | class TestStarttls < Test::Unit::TestCase 11 | class MySMTP < SMTP 12 | def initialize(socket) 13 | @fake_socket = socket 14 | super("smtp.example.com") 15 | end 16 | 17 | def tcp_socket(*) 18 | @fake_socket 19 | end 20 | 21 | def tlsconnect(*) 22 | @fake_socket 23 | end 24 | end 25 | 26 | def teardown 27 | @server_thread&.exit&.join 28 | @server_socket&.close 29 | @client_socket&.close 30 | end 31 | 32 | def start_smtpd(starttls) 33 | @server_socket, @client_socket = Object.const_defined?(:UNIXSocket) ? 34 | UNIXSocket.pair : Socket.pair(:INET, :STREAM, 0) 35 | @starttls_executed = false 36 | @server_thread = Thread.new(@server_socket) do |s| 37 | s.puts "220 fakeserver\r\n" 38 | while cmd = s.gets&.chomp 39 | case cmd 40 | when /\AEHLO / 41 | s.puts "250-fakeserver\r\n" 42 | s.puts "250-STARTTLS\r\n" if starttls 43 | s.puts "250 8BITMIME\r\n" 44 | when /\ASTARTTLS/ 45 | @starttls_executed = true 46 | s.puts "220 2.0.0 Ready to start TLS\r\n" 47 | else 48 | raise "unsupported command: #{cmd}" 49 | end 50 | end 51 | end 52 | @client_socket 53 | end 54 | 55 | def test_default_with_starttls_capable 56 | smtp = MySMTP.new(start_smtpd(true)) 57 | smtp.start 58 | assert(@starttls_executed) 59 | end 60 | 61 | def test_default_without_starttls_capable 62 | smtp = MySMTP.new(start_smtpd(false)) 63 | smtp.start 64 | assert(!@starttls_executed) 65 | end 66 | 67 | def test_enable_starttls_with_starttls_capable 68 | smtp = MySMTP.new(start_smtpd(true)) 69 | smtp.enable_starttls 70 | smtp.start 71 | assert(@starttls_executed) 72 | end 73 | 74 | def test_enable_starttls_without_starttls_capable 75 | smtp = MySMTP.new(start_smtpd(false)) 76 | smtp.enable_starttls 77 | err = assert_raise(Net::SMTPUnsupportedCommand) { smtp.start } 78 | assert_equal("STARTTLS is not supported on this server", err.message) 79 | assert_nil(err.response) 80 | end 81 | 82 | def test_enable_starttls_auto_with_starttls_capable 83 | smtp = MySMTP.new(start_smtpd(true)) 84 | smtp.enable_starttls_auto 85 | smtp.start 86 | assert(@starttls_executed) 87 | end 88 | 89 | def test_tls_with_starttls_capable 90 | smtp = MySMTP.new(start_smtpd(true)) 91 | smtp.enable_tls 92 | smtp.start 93 | assert(!@starttls_executed) 94 | end 95 | 96 | def test_tls_without_starttls_capable 97 | smtp = MySMTP.new(start_smtpd(false)) 98 | smtp.enable_tls 99 | end 100 | 101 | def test_disable_starttls 102 | smtp = MySMTP.new(start_smtpd(true)) 103 | smtp.disable_starttls 104 | smtp.start 105 | assert(!@starttls_executed) 106 | end 107 | 108 | def test_enable_tls_and_enable_starttls 109 | smtp = MySMTP.new(start_smtpd(true)) 110 | smtp.enable_tls 111 | err = assert_raise(ArgumentError) { smtp.enable_starttls } 112 | assert_equal("SMTPS and STARTTLS is exclusive", err.message) 113 | end 114 | 115 | def test_enable_tls_and_enable_starttls_auto 116 | smtp = MySMTP.new(start_smtpd(true)) 117 | smtp.enable_tls 118 | err = assert_raise(ArgumentError) { smtp.enable_starttls_auto } 119 | assert_equal("SMTPS and STARTTLS is exclusive", err.message) 120 | end 121 | 122 | def test_enable_starttls_and_enable_starttls_auto 123 | smtp = MySMTP.new(start_smtpd(true)) 124 | smtp.enable_starttls 125 | assert_nothing_raised { smtp.enable_starttls_auto } 126 | end 127 | end 128 | end 129 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # NEWS 2 | 3 | ## Version 0.5.0 (2024-03-27) 4 | 5 | ### Improvements 6 | 7 | * Allow case-insensitive strings for SASL mechanism 8 | * Make #auth_capable? public 9 | * Add XOAUTH2 authenticator 10 | 11 | ### Others 12 | 13 | * Remove unused private auth_method 14 | * Delegate checking auth args to the authenticator 15 | * Updated docs, especially TLS and SASL-related 16 | * Renew test certificates 17 | * Fix version extraction to work with non ASCII characters with any LANG 18 | * Replace non-ASCII EM DASH (U+2014) with ASCII hyphen (U+002D) 19 | * Use reusing workflow for Ruby versions 20 | * Make the test suite compatible with --enable-frozen-string-literal 21 | 22 | ## Version 0.4.0 (2023-09-20) 23 | 24 | ### Improvements 25 | 26 | * add Net::SMTP::Authenticator class and auth_* methods are separated from the Net::SMTP class. 27 | This allows you to add a new authentication method to Net::SMTP. 28 | Create a class with an `auth` method that inherits Net::SMTP::Authenticator. 29 | The `auth` method has two arguments, `user` and `secret`. 30 | Send an instruction to the SMTP server by using the `continue` or `finish` method. 31 | For more information, see lib/net/smtp/auto _*.rb. 32 | * Add SMTPUTF8 support 33 | 34 | ### Fixes 35 | 36 | * Revert "Replace Timeout.timeout with socket timeout" 37 | * Fixed issue sending emails to unaffected recipients on 53x error 38 | 39 | ### Others 40 | 41 | * Removed unnecessary Subversion keywords 42 | 43 | ## Version 0.3.3 (2022-10-29) 44 | 45 | * No timeout library required 46 | * Make the digest library optional 47 | 48 | ## Version 0.3.2 (2022-09-28) 49 | 50 | * Make exception API compatible with what Ruby expects 51 | 52 | ## Version 0.3.1 (2021-12-12) 53 | 54 | ### Improvements 55 | 56 | * add Net::SMTP::Address. 57 | * add Net::SMTP#capable? and Net::SMTP#capabilities. 58 | * add Net::SMTP#tls_verify, Net::SMTP#tls_hostname, Net::SMTP#ssl_context_params 59 | 60 | ## Version 0.3.0 (2021-10-14) 61 | 62 | ### Improvements 63 | 64 | * Add `tls`, `starttls` keyword arguments. 65 | ```ruby 66 | # always use TLS connection for port 465. 67 | Net::SMTP.start(hostname, 465, tls: true) 68 | 69 | # do not use starttls for localhost 70 | Net::SMTP.start('localhost', starttls: false) 71 | ``` 72 | 73 | ### Incompatible changes 74 | 75 | * The tls_* paramter has been moved from start() to initialize(). 76 | 77 | ## Version 0.2.2 (2021-10-09) 78 | 79 | * Add `response` to SMTPError exceptions. 80 | * `Net::SMTP.start()` and `#start()` accepts `ssl_context_params` keyword argument. 81 | * Replace `Timeout.timeout` with socket timeout. 82 | * Remove needless files from gem. 83 | * Add dependency on digest, timeout. 84 | 85 | ## Version 0.2.1 (2020-11-18) 86 | 87 | ### Fixes 88 | 89 | * Update the license for the default gems to dual licenses. 90 | * Add dependency for net-protocol. 91 | 92 | ## Version 0.2.0 (2020-11-15) 93 | 94 | ### Incompatible changes 95 | 96 | * Verify the server's certificate by default. 97 | If you don't want verification, specify `start(tls_verify: false)`. 98 | 99 | 100 | * Use STARTTLS by default if possible. 101 | If you don't want starttls, specify: 102 | ``` 103 | smtp = Net::SMTP.new(hostname, port) 104 | smtp.disable_starttls 105 | smtp.start do |s| 106 | s.send_message .... 107 | end 108 | ``` 109 | 110 | 111 | ### Improvements 112 | 113 | * Net::SMTP.start and Net::SMTP#start arguments are keyword arguments. 114 | ``` 115 | start(address, port = nil, helo: 'localhost', user: nil, secret: nil, authtype: nil) { |smtp| ... } 116 | ``` 117 | `password` is an alias of `secret`. 118 | 119 | 120 | * Add `tls_hostname` parameter to `start()`. 121 | If you want to use a different hostname than the certificate for the connection, you can specify the certificate hostname with `tls_hostname`. 122 | 123 | 124 | * Add SNI support to net/smtp 125 | 126 | ### Fixes 127 | 128 | * enable_starttls before disable_tls causes an error. 129 | * TLS should not check the hostname when verify_mode is disabled. 130 | 131 | ## Version 0.1.0 (2019-12-03) 132 | 133 | This is the first release of net-smtp gem. 134 | -------------------------------------------------------------------------------- /test/net/smtp/test_sslcontext.rb: -------------------------------------------------------------------------------- 1 | require 'net/smtp' 2 | require 'test/unit' 3 | 4 | unless defined?(OpenSSL::VERSION) 5 | warn "#{__FILE__}: openssl library not loaded; skipping" 6 | return 7 | end 8 | 9 | module Net 10 | class TestSSLContext < Test::Unit::TestCase 11 | # SERVER_CERT's subject has CN=localhost 12 | CA_FILE = File.expand_path("../fixtures/cacert.pem", __dir__) 13 | SERVER_KEY = File.expand_path("../fixtures/server.key", __dir__) 14 | SERVER_CERT = File.expand_path("../fixtures/server.crt", __dir__) 15 | 16 | class MySMTP < SMTP 17 | attr_reader :__ssl_context, :__ssl_socket 18 | 19 | def initialize(socket, **kw) 20 | @fake_socket = socket 21 | super("localhost", **kw) 22 | end 23 | 24 | def tcp_socket(*) 25 | @fake_socket 26 | end 27 | 28 | def ssl_socket(socket, context) 29 | @__ssl_context = context 30 | @__ssl_socket = super 31 | end 32 | end 33 | 34 | def teardown 35 | @server_thread&.exit&.join 36 | @server_socket&.close 37 | @client_socket&.close 38 | end 39 | 40 | private def default_ssl_context 41 | store = OpenSSL::X509::Store.new 42 | store.add_file(CA_FILE) 43 | SMTP.default_ssl_context(cert_store: store) 44 | end 45 | 46 | private def wrap_ssl_socket(sock) 47 | ctx = OpenSSL::SSL::SSLContext.new 48 | ctx.add_certificate( 49 | OpenSSL::X509::Certificate.new(File.read(SERVER_CERT)), 50 | OpenSSL::PKey.read(File.read(SERVER_KEY)), 51 | [OpenSSL::X509::Certificate.new(File.read(CA_FILE))]) 52 | sock = OpenSSL::SSL::SSLSocket.new(sock, ctx) 53 | sock.sync_close = true 54 | sock.accept 55 | rescue OpenSSL::SSL::SSLError 56 | # The client must be raising SSLError, too 57 | end 58 | 59 | def start_smtpd_starttls 60 | @server_socket, @client_socket = Object.const_defined?(:UNIXSocket) ? 61 | UNIXSocket.pair : Socket.pair(:INET, :STREAM, 0) 62 | @server_thread = Thread.new(@server_socket) do |s| 63 | s.puts "220 fakeserver\r\n" 64 | while cmd = s.gets&.chomp 65 | case cmd 66 | when /\AEHLO / 67 | s.puts "250-fakeserver\r\n" 68 | s.puts "250-STARTTLS\r\n" 69 | s.puts "250 8BITMIME\r\n" 70 | when /\ARSET/ 71 | s.puts "250 OK\r\n" 72 | when /\ASTARTTLS/ 73 | s.puts "220 2.0.0 Ready to start TLS\r\n" 74 | s = wrap_ssl_socket(s) or break 75 | else 76 | raise "unsupported command: #{cmd}" 77 | end 78 | end 79 | end 80 | @client_socket 81 | end 82 | 83 | def start_smtpd_smtps 84 | @server_socket, @client_socket = Object.const_defined?(:UNIXSocket) ? 85 | UNIXSocket.pair : Socket.pair(:INET, :STREAM, 0) 86 | @server_thread = Thread.new(@server_socket) do |s| 87 | s = wrap_ssl_socket(s) or break 88 | s.puts "220 fakeserver\r\n" 89 | while cmd = s.gets&.chomp 90 | case cmd 91 | when /\AEHLO / 92 | s.puts "250-fakeserver\r\n" 93 | s.puts "250 8BITMIME\r\n" 94 | when /\ARSET/ 95 | s.puts "250 OK\r\n" 96 | else 97 | raise "unsupported command: #{cmd}" 98 | end 99 | end 100 | end 101 | @client_socket 102 | end 103 | 104 | def test_default 105 | smtp = MySMTP.new(start_smtpd_starttls) 106 | assert_raise(OpenSSL::SSL::SSLError) { smtp.start } 107 | assert_equal(OpenSSL::X509::V_ERR_SELF_SIGNED_CERT_IN_CHAIN, smtp.__ssl_socket.verify_result) 108 | assert_equal(OpenSSL::SSL::VERIFY_PEER, smtp.__ssl_context.verify_mode) 109 | end 110 | 111 | def test_starttls_close_socket_on_verify_failure 112 | smtp = MySMTP.new(start_smtpd_starttls) 113 | assert_raise(OpenSSL::SSL::SSLError) { smtp.start } 114 | assert_equal(true, smtp.__ssl_socket.closed?) 115 | end 116 | 117 | def test_enable_tls 118 | smtp = MySMTP.new(start_smtpd_smtps) 119 | context = default_ssl_context 120 | smtp.enable_tls(context) 121 | smtp.start 122 | assert_equal(context, smtp.__ssl_context) 123 | assert_equal(true, smtp.rset.success?) 124 | end 125 | 126 | def test_enable_tls_before_disable_starttls 127 | smtp = MySMTP.new(start_smtpd_smtps) 128 | context = default_ssl_context 129 | smtp.enable_tls(context) 130 | smtp.disable_starttls 131 | smtp.start 132 | assert_equal(context, smtp.__ssl_context) 133 | assert_equal(true, smtp.rset.success?) 134 | end 135 | 136 | def test_enable_starttls 137 | smtp = MySMTP.new(start_smtpd_starttls) 138 | context = default_ssl_context 139 | smtp.enable_starttls(context) 140 | smtp.start 141 | assert_equal(context, smtp.__ssl_context) 142 | assert_equal(true, smtp.rset.success?) 143 | end 144 | 145 | def test_enable_starttls_before_disable_tls 146 | smtp = MySMTP.new(start_smtpd_starttls) 147 | context = default_ssl_context 148 | smtp.enable_starttls(context) 149 | smtp.disable_tls 150 | smtp.start 151 | assert_equal(context, smtp.__ssl_context) 152 | assert_equal(true, smtp.rset.success?) 153 | end 154 | 155 | def test_start_with_tls_verify_true 156 | smtp = MySMTP.new(start_smtpd_starttls, tls_verify: true) 157 | assert_raise(OpenSSL::SSL::SSLError) { smtp.start } 158 | assert_equal(OpenSSL::X509::V_ERR_SELF_SIGNED_CERT_IN_CHAIN, smtp.__ssl_socket.verify_result) 159 | assert_equal(OpenSSL::SSL::VERIFY_PEER, smtp.__ssl_context.verify_mode) 160 | end 161 | 162 | def test_start_with_tls_verify_false 163 | smtp = MySMTP.new(start_smtpd_starttls, tls_verify: false) 164 | smtp.start 165 | assert_equal(OpenSSL::SSL::VERIFY_NONE, smtp.__ssl_context.verify_mode) 166 | assert_equal(true, smtp.rset.success?) 167 | end 168 | 169 | def test_tls_verify_true_after_initialize 170 | smtp = MySMTP.new(start_smtpd_starttls, tls_verify: false) 171 | smtp.tls_verify = true 172 | assert_raise(OpenSSL::SSL::SSLError) { smtp.start } 173 | assert_equal(OpenSSL::X509::V_ERR_SELF_SIGNED_CERT_IN_CHAIN, smtp.__ssl_socket.verify_result) 174 | assert_equal(OpenSSL::SSL::VERIFY_PEER, smtp.__ssl_context.verify_mode) 175 | end 176 | 177 | def test_tls_verify_false_after_initialize 178 | smtp = MySMTP.new(start_smtpd_starttls, tls_verify: true) 179 | smtp.tls_verify = false 180 | smtp.start 181 | assert_equal(OpenSSL::SSL::VERIFY_NONE, smtp.__ssl_context.verify_mode) 182 | assert_equal(true, smtp.rset.success?) 183 | end 184 | 185 | def test_start_with_tls_hostname 186 | smtp = MySMTP.new(start_smtpd_starttls, tls_hostname: "unexpected.example.com") 187 | context = default_ssl_context 188 | smtp.enable_starttls(context) 189 | assert_raise(OpenSSL::SSL::SSLError) { smtp.start } 190 | # TODO: Not all OpenSSL versions have the same verify_result code 191 | assert_equal("unexpected.example.com", smtp.__ssl_socket.hostname) 192 | end 193 | 194 | def test_start_without_tls_hostname 195 | smtp = MySMTP.new(start_smtpd_starttls) 196 | context = default_ssl_context 197 | smtp.enable_starttls(context) 198 | smtp.start 199 | assert_equal("localhost", smtp.__ssl_socket.hostname) 200 | assert_equal(true, smtp.rset.success?) 201 | end 202 | 203 | def test_tls_hostname_after_initialize 204 | smtp = MySMTP.new(start_smtpd_starttls) 205 | smtp.tls_hostname = "unexpected.example.com" 206 | context = default_ssl_context 207 | smtp.enable_starttls(context) 208 | assert_raise(OpenSSL::SSL::SSLError) { smtp.start } 209 | # TODO: Not all OpenSSL versions have the same verify_result code 210 | assert_equal("unexpected.example.com", smtp.__ssl_socket.hostname) 211 | end 212 | 213 | def test_start_with_ssl_context_params 214 | smtp = MySMTP.new(start_smtpd_starttls, ssl_context_params: {timeout: 123, verify_mode: OpenSSL::SSL::VERIFY_NONE}) 215 | smtp.start 216 | assert_equal(123, smtp.__ssl_context.timeout) 217 | end 218 | 219 | def test_ssl_context_params_after_initialize 220 | smtp = MySMTP.new(start_smtpd_starttls) 221 | smtp.ssl_context_params = {timeout: 123, verify_mode: OpenSSL::SSL::VERIFY_NONE} 222 | smtp.start 223 | assert_equal(123, smtp.__ssl_context.timeout) 224 | end 225 | end 226 | end 227 | -------------------------------------------------------------------------------- /test/net/smtp/test_smtp.rb: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | 3 | require 'net/smtp' 4 | require 'test/unit' 5 | 6 | module Net 7 | class TestSMTP < Test::Unit::TestCase 8 | def setup 9 | # Avoid hanging at fake_server_start's IO.select on --jit-wait CI like http://ci.rvm.jp/results/trunk-mjit-wait@phosphorus-docker/3302796 10 | # Unfortunately there's no way to configure read_timeout for Net::SMTP.start. 11 | if defined?(RubyVM::JIT) && RubyVM::JIT.enabled? 12 | Net::SMTP.prepend Module.new { 13 | def initialize(*) 14 | super 15 | @read_timeout *= 5 16 | end 17 | } 18 | end 19 | end 20 | 21 | def teardown 22 | FakeServer.stop_all 23 | end 24 | 25 | def test_critical 26 | smtp = Net::SMTP.new 'localhost', 25 27 | 28 | assert_raise RuntimeError do 29 | smtp.send :critical do 30 | raise 'fail on purpose' 31 | end 32 | end 33 | 34 | assert_kind_of Net::SMTP::Response, smtp.send(:critical), 35 | '[Bug #9125]' 36 | end 37 | 38 | def test_esmtp 39 | smtp = Net::SMTP.new 'localhost', 25 40 | assert smtp.esmtp 41 | assert smtp.esmtp? 42 | 43 | smtp.esmtp = 'omg' 44 | assert_equal 'omg', smtp.esmtp 45 | assert_equal 'omg', smtp.esmtp? 46 | end 47 | 48 | def test_server_capabilities 49 | if defined? OpenSSL 50 | port = fake_server_start(starttls: true, auth: 'plain') 51 | smtp = Net::SMTP.start('localhost', port, starttls: false) 52 | assert_equal({"STARTTLS"=>[], "AUTH"=>["PLAIN"]}, smtp.capabilities) 53 | assert_equal(true, smtp.capable?('STARTTLS')) 54 | assert_equal(false, smtp.capable?('DOES-NOT-EXIST')) 55 | else 56 | port = fake_server_start 57 | smtp = Net::SMTP.start('localhost', port, starttls: false) 58 | assert_equal({"AUTH"=>["PLAIN"]}, smtp.capabilities) 59 | assert_equal(false, smtp.capable?('STARTTLS')) 60 | assert_equal(false, smtp.capable?('DOES-NOT-EXIST')) 61 | end 62 | smtp.finish 63 | end 64 | 65 | def test_rset 66 | smtp = Net::SMTP.start 'localhost', fake_server_start 67 | assert smtp.rset 68 | smtp.finish 69 | end 70 | 71 | def test_mailfrom 72 | server = FakeServer.start 73 | smtp = Net::SMTP.start 'localhost', server.port 74 | assert smtp.mailfrom("foo@example.com").success? 75 | assert_equal "MAIL FROM:\r\n", server.commands.last 76 | smtp.finish 77 | end 78 | 79 | def test_mailfrom_with_address 80 | server = FakeServer.start 81 | smtp = Net::SMTP.start 'localhost', server.port 82 | addr = Net::SMTP::Address.new("foo@example.com", size: 12345) 83 | assert smtp.mailfrom(addr).success? 84 | assert_equal "MAIL FROM: size=12345\r\n", server.commands.last 85 | end 86 | 87 | def test_rcptto 88 | server = FakeServer.start 89 | smtp = Net::SMTP.start 'localhost', server.port 90 | assert smtp.rcptto("foo@example.com").success? 91 | assert_equal "RCPT TO:\r\n", server.commands.last 92 | end 93 | 94 | def test_rcptto_with_address 95 | server = FakeServer.start 96 | smtp = Net::SMTP.start 'localhost', server.port 97 | addr = Net::SMTP::Address.new("foo@example.com", nofty: :failure) 98 | assert smtp.rcptto(addr).success? 99 | assert_equal "RCPT TO: nofty=failure\r\n", server.commands.last 100 | end 101 | 102 | def test_address 103 | a = Net::SMTP::Address.new('foo@example.com', 'p0=123', {p1: 456}, p2: nil, p3: '789') 104 | assert_equal 'foo@example.com', a.address 105 | assert_equal ['p0=123', 'p1=456', 'p2', 'p3=789'], a.parameters 106 | end 107 | 108 | def test_auth_plain 109 | server = FakeServer.start(auth: 'plain') 110 | smtp = Net::SMTP.start 'localhost', server.port 111 | assert smtp.authenticate("account", "password", :plain).success? 112 | assert_equal "AUTH PLAIN AGFjY291bnQAcGFzc3dvcmQ=\r\n", server.commands.last 113 | end 114 | 115 | def test_unsuccessful_auth_plain 116 | server = FakeServer.start(auth: 'plain') 117 | smtp = Net::SMTP.start 'localhost', server.port 118 | err = assert_raise(Net::SMTPAuthenticationError) { smtp.authenticate("foo", "bar", :plain) } 119 | assert_equal "535 5.7.8 Error: authentication failed: authentication failure\n", err.message 120 | assert_equal "535", err.response.status 121 | end 122 | 123 | def test_auth_login 124 | server = FakeServer.start(auth: 'login') 125 | smtp = Net::SMTP.start 'localhost', server.port 126 | assert smtp.authenticate("account", "password", :login).success? 127 | end 128 | 129 | def test_unsuccessful_auth_login 130 | server = FakeServer.start(auth: 'login') 131 | smtp = Net::SMTP.start 'localhost', server.port 132 | err = assert_raise(Net::SMTPAuthenticationError) { smtp.authenticate("foo", "bar", :login) } 133 | assert_equal "535 5.7.8 Error: authentication failed: authentication failure\n", err.message 134 | assert_equal "535", err.response.status 135 | end 136 | 137 | def test_non_continue_auth_login 138 | server = FakeServer.start(auth: 'login') 139 | def server.auth(*) 140 | @sock.puts "334 VXNlcm5hbWU6\r\n" 141 | @sock.gets 142 | @sock.puts "235 2.7.0 Authentication successful\r\n" 143 | end 144 | smtp = Net::SMTP.start 'localhost', server.port 145 | err = assert_raise(Net::SMTPUnknownError) { smtp.authenticate("account", "password", :login) } 146 | assert_equal "235 2.7.0 Authentication successful\n", err.message 147 | assert_equal "235", err.response.status 148 | end 149 | 150 | def test_auth_xoauth2 151 | server = FakeServer.start(auth: 'xoauth2') 152 | smtp = Net::SMTP.start 'localhost', server.port 153 | assert smtp.authenticate("account", "token", :xoauth2).success? 154 | assert_equal "AUTH XOAUTH2 dXNlcj1hY2NvdW50AWF1dGg9QmVhcmVyIHRva2VuAQE=\r\n", server.commands.last 155 | end 156 | 157 | def test_unsuccessful_auth_xoauth2 158 | server = FakeServer.start(auth: 'xoauth2') 159 | smtp = Net::SMTP.start 'localhost', server.port 160 | err = assert_raise(Net::SMTPAuthenticationError) { smtp.authenticate("account", "password", :xoauth2) } 161 | assert_equal "535 5.7.8 Error: authentication failed: authentication failure\n", err.message 162 | assert_equal "535", err.response.status 163 | end 164 | 165 | def test_send_message 166 | port = fake_server_start 167 | smtp = Net::SMTP.start 'localhost', port 168 | assert_nothing_raised do 169 | smtp.send_message("message", "sender@example.com", "rcpt1@example.com") 170 | end 171 | end 172 | 173 | def test_send_message_with_multiple_recipients 174 | port = fake_server_start 175 | smtp = Net::SMTP.start 'localhost', port 176 | assert_nothing_raised do 177 | smtp.send_message("message", "sender@example.com", "rcpt1@example.com", "rcpt2@example.com") 178 | end 179 | end 180 | 181 | def test_send_message_with_multiple_recipients_as_array 182 | port = fake_server_start 183 | smtp = Net::SMTP.start 'localhost', port 184 | assert_nothing_raised do 185 | smtp.send_message("message", "sender@example.com", ["rcpt1@example.com", "rcpt2@example.com"]) 186 | end 187 | end 188 | 189 | def test_unsuccessful_send_message_server_busy 190 | server = FakeServer.new 191 | def server.greeting 192 | @sock.puts "400 BUSY\r\n" 193 | end 194 | server.start 195 | err = assert_raise(Net::SMTPServerBusy) { Net::SMTP.start 'localhost', server.port } 196 | assert_equal "400 BUSY\n", err.message 197 | assert_equal "400", err.response.status 198 | end 199 | 200 | def test_unsuccessful_send_message_syntax_error 201 | server = FakeServer.new 202 | def server.greeting 203 | @sock.puts "502 SYNTAX ERROR\r\n" 204 | end 205 | server.start 206 | err = assert_raise(Net::SMTPSyntaxError) { Net::SMTP.start 'localhost', server.port } 207 | assert_equal "502 SYNTAX ERROR\n", err.message 208 | assert_equal "502", err.response.status 209 | end 210 | 211 | def test_unsuccessful_send_message_authentication_error 212 | server = FakeServer.new 213 | def server.greeting 214 | @sock.puts "530 AUTH ERROR\r\n" 215 | end 216 | server.start 217 | err = assert_raise(Net::SMTPAuthenticationError) { Net::SMTP.start 'localhost', server.port } 218 | assert_equal "530 AUTH ERROR\n", err.message 219 | assert_equal "530", err.response.status 220 | end 221 | 222 | def test_unsuccessful_send_message_fatal_error 223 | server = FakeServer.new 224 | def server.greeting 225 | @sock.puts "520 FATAL ERROR\r\n" 226 | end 227 | server.start 228 | err = assert_raise(Net::SMTPFatalError) { Net::SMTP.start 'localhost', server.port } 229 | assert_equal "520 FATAL ERROR\n", err.message 230 | assert_equal "520", err.response.status 231 | end 232 | 233 | def test_unsuccessful_send_message_unknown_error 234 | server = FakeServer.new 235 | def server.greeting 236 | @sock.puts "300 UNKNOWN\r\n" 237 | end 238 | server.start 239 | err = assert_raise(Net::SMTPUnknownError) { Net::SMTP.start 'localhost', server.port } 240 | assert_equal "300 UNKNOWN\n", err.message 241 | assert_equal "300", err.response.status 242 | end 243 | 244 | def test_unsuccessful_data 245 | server = FakeServer.new 246 | def server.data 247 | @sock.puts "250 OK\r\n" 248 | end 249 | server.start 250 | smtp = Net::SMTP.start 'localhost', server.port 251 | err = assert_raise(Net::SMTPUnknownError) { smtp.data('message') } 252 | assert_equal "could not get 3xx (250: 250 OK\n)", err.message 253 | assert_equal "250", err.response.status 254 | end 255 | 256 | def test_crlf_injection 257 | server = FakeServer.new 258 | smtp = Net::SMTP.new 'localhost', server.port 259 | 260 | assert_raise(ArgumentError) do 261 | smtp.mailfrom("foo\r\nbar") 262 | end 263 | 264 | assert_raise(ArgumentError) do 265 | smtp.mailfrom("foo\rbar") 266 | end 267 | 268 | assert_raise(ArgumentError) do 269 | smtp.mailfrom("foo\nbar") 270 | end 271 | 272 | assert_raise(ArgumentError) do 273 | smtp.rcptto("foo\r\nbar") 274 | end 275 | end 276 | 277 | def test_tls_connect 278 | omit "openssl library not loaded" unless defined?(OpenSSL::VERSION) 279 | 280 | server = FakeServer.start(tls: true) 281 | smtp = Net::SMTP.new("localhost", server.port, tls_verify: false) 282 | smtp.enable_tls 283 | smtp.open_timeout = 1 284 | smtp.start{} 285 | ensure 286 | server.stop 287 | end 288 | 289 | def test_tls_connect_timeout 290 | omit "openssl library not loaded" unless defined?(OpenSSL::VERSION) 291 | 292 | server = FakeServer.new 293 | def server.init 294 | sleep 295 | end 296 | server.start(tls: true) 297 | smtp = Net::SMTP.new("localhost", server.port) 298 | smtp.enable_tls 299 | smtp.open_timeout = 0.1 300 | assert_raise(Net::OpenTimeout) do 301 | smtp.start{} 302 | end 303 | ensure 304 | server.stop 305 | end 306 | 307 | def test_eof_error_backtrace 308 | bug13018 = '[ruby-core:78550] [Bug #13018]' 309 | 310 | server = FakeServer.new 311 | def server.ehlo(*) 312 | @sock.shutdown(:WR) 313 | end 314 | 315 | begin 316 | server.start 317 | smtp = Net::SMTP.new("localhost", server.port) 318 | e = assert_raise(EOFError, bug13018) do 319 | smtp.start{} 320 | end 321 | assert_equal(EOFError, e.class, bug13018) 322 | assert(e.backtrace.grep(%r"\bnet/smtp\.rb:").size > 0, bug13018) 323 | ensure 324 | server.stop 325 | end 326 | end 327 | 328 | def test_with_tls 329 | omit "openssl library not loaded" unless defined?(OpenSSL::VERSION) 330 | 331 | server = FakeServer.start(tls: true) 332 | smtp = Net::SMTP.new('localhost', server.port, tls: true, tls_verify: false) 333 | assert_nothing_raised do 334 | smtp.start{} 335 | end 336 | 337 | server = FakeServer.start(tls: false) 338 | smtp = Net::SMTP.new('localhost', server.port, tls: false) 339 | assert_nothing_raised do 340 | smtp.start{} 341 | end 342 | end 343 | 344 | def test_with_starttls_always 345 | omit "openssl library not loaded" unless defined?(OpenSSL::VERSION) 346 | 347 | server = FakeServer.start(starttls: true) 348 | smtp = Net::SMTP.new('localhost', server.port, starttls: :always, tls_verify: false) 349 | smtp.start{} 350 | assert_equal(true, server.starttls_started?) 351 | 352 | server = FakeServer.start(starttls: false) 353 | smtp = Net::SMTP.new('localhost', server.port, starttls: :always, tls_verify: false) 354 | assert_raise Net::SMTPUnsupportedCommand do 355 | smtp.start{} 356 | end 357 | end 358 | 359 | def test_with_starttls_auto 360 | omit "openssl library not loaded" unless defined?(OpenSSL::VERSION) 361 | 362 | server = FakeServer.start(starttls: true) 363 | smtp = Net::SMTP.new('localhost', server.port, starttls: :auto, tls_verify: false) 364 | smtp.start{} 365 | assert_equal(true, server.starttls_started?) 366 | 367 | server = FakeServer.start(starttls: false) 368 | smtp = Net::SMTP.new('localhost', server.port, starttls: :auto, tls_verify: false) 369 | smtp.start{} 370 | assert_equal(false, server.starttls_started?) 371 | end 372 | 373 | def test_with_starttls_false 374 | omit "openssl library not loaded" unless defined?(OpenSSL::VERSION) 375 | 376 | server = FakeServer.start(starttls: true) 377 | smtp = Net::SMTP.new('localhost', server.port, starttls: false, tls_verify: false) 378 | smtp.start{} 379 | assert_equal(false, server.starttls_started?) 380 | 381 | server = FakeServer.start(starttls: false) 382 | smtp = Net::SMTP.new('localhost', server.port, starttls: false, tls_verify: false) 383 | smtp.start{} 384 | assert_equal(false, server.starttls_started?) 385 | end 386 | 387 | def test_start 388 | port = fake_server_start 389 | smtp = Net::SMTP.start('localhost', port) 390 | smtp.finish 391 | end 392 | 393 | def test_start_with_position_argument 394 | port = fake_server_start(auth: 'plain') 395 | smtp = Net::SMTP.start('localhost', port, 'myname', 'account', 'password', :plain) 396 | smtp.finish 397 | end 398 | 399 | def test_start_with_keyword_argument 400 | port = fake_server_start(auth: 'plain') 401 | smtp = Net::SMTP.start('localhost', port, helo: 'myname', user: 'account', secret: 'password', authtype: :plain) 402 | smtp.finish 403 | end 404 | 405 | def test_start_password_is_secret 406 | port = fake_server_start(auth: 'plain') 407 | smtp = Net::SMTP.start('localhost', port, helo: 'myname', user: 'account', password: 'password', authtype: :plain) 408 | smtp.finish 409 | end 410 | 411 | def test_start_invalid_number_of_arguments 412 | err = assert_raise ArgumentError do 413 | Net::SMTP.start('localhost', 25, 'myname', 'account', 'password', :plain, :invalid_arg) 414 | end 415 | assert_equal('wrong number of arguments (given 7, expected 1..6)', err.message) 416 | end 417 | 418 | def test_start_with_tls 419 | omit "openssl library not loaded" unless defined?(OpenSSL::VERSION) 420 | 421 | port = fake_server_start(tls: true) 422 | assert_nothing_raised do 423 | Net::SMTP.start('localhost', port, tls: true, tls_verify: false){} 424 | end 425 | 426 | port = fake_server_start(tls: false) 427 | assert_nothing_raised do 428 | Net::SMTP.start('localhost', port, tls: false){} 429 | end 430 | end 431 | 432 | def test_start_with_starttls_always 433 | omit "openssl library not loaded" unless defined?(OpenSSL::VERSION) 434 | 435 | server = FakeServer.start(starttls: true) 436 | Net::SMTP.start('localhost', server.port, starttls: :always, tls_verify: false){} 437 | assert_equal(true, server.starttls_started?) 438 | 439 | server = FakeServer.start(starttls: false) 440 | assert_raise Net::SMTPUnsupportedCommand do 441 | Net::SMTP.start('localhost', server.port, starttls: :always, tls_verify: false){} 442 | end 443 | end 444 | 445 | def test_start_with_starttls_auto 446 | omit "openssl library not loaded" unless defined?(OpenSSL::VERSION) 447 | 448 | server = FakeServer.start(starttls: true) 449 | Net::SMTP.start('localhost', server.port, starttls: :auto, tls_verify: false){} 450 | assert_equal(true, server.starttls_started?) 451 | 452 | server = FakeServer.start(starttls: false) 453 | Net::SMTP.start('localhost', server.port, starttls: :auto, tls_verify: false){} 454 | assert_equal(false, server.starttls_started?) 455 | end 456 | 457 | def test_start_with_starttls_false 458 | omit "openssl library not loaded" unless defined?(OpenSSL::VERSION) 459 | 460 | server = FakeServer.start(starttls: true) 461 | Net::SMTP.start('localhost', server.port, starttls: false, tls_verify: false){} 462 | assert_equal(false, server.starttls_started?) 463 | 464 | server = FakeServer.start(starttls: false) 465 | Net::SMTP.start('localhost', server.port, starttls: false, tls_verify: false){} 466 | assert_equal(false, server.starttls_started?) 467 | end 468 | 469 | def test_start_auth_plain 470 | port = fake_server_start(auth: 'plain') 471 | Net::SMTP.start('localhost', port, user: 'account', password: 'password', authtype: :plain){} 472 | 473 | port = fake_server_start(auth: 'plain') 474 | assert_raise Net::SMTPAuthenticationError do 475 | Net::SMTP.start('localhost', port, user: 'account', password: 'invalid', authtype: :plain){} 476 | end 477 | 478 | port = fake_server_start(auth: 'login') 479 | assert_raise Net::SMTPAuthenticationError do 480 | Net::SMTP.start('localhost', port, user: 'account', password: 'password', authtype: :plain){} 481 | end 482 | end 483 | 484 | def test_start_auth_login 485 | port = fake_server_start(auth: 'LOGIN') 486 | Net::SMTP.start('localhost', port, user: 'account', password: 'password', authtype: :login){} 487 | 488 | port = fake_server_start(auth: 'LOGIN') 489 | assert_raise Net::SMTPAuthenticationError do 490 | Net::SMTP.start('localhost', port, user: 'account', password: 'invalid', authtype: :login){} 491 | end 492 | 493 | port = fake_server_start(auth: 'PLAIN') 494 | assert_raise Net::SMTPAuthenticationError do 495 | Net::SMTP.start('localhost', port, user: 'account', password: 'password', authtype: :login){} 496 | end 497 | end 498 | 499 | def test_start_auth_cram_md5 500 | omit "openssl or digest library not loaded" unless defined? OpenSSL or defined? Digest 501 | 502 | port = fake_server_start(auth: 'CRAM-MD5') 503 | Net::SMTP.start('localhost', port, user: 'account', password: 'password', authtype: "CRAM-MD5"){} 504 | 505 | port = fake_server_start(auth: 'CRAM-MD5') 506 | assert_raise Net::SMTPAuthenticationError do 507 | Net::SMTP.start('localhost', port, user: 'account', password: 'invalid', authtype: :cram_md5){} 508 | end 509 | 510 | port = fake_server_start(auth: 'PLAIN') 511 | assert_raise Net::SMTPAuthenticationError do 512 | Net::SMTP.start('localhost', port, user: 'account', password: 'password', authtype: :cram_md5){} 513 | end 514 | 515 | port = fake_server_start(auth: 'CRAM-MD5') 516 | smtp = Net::SMTP.new('localhost', port) 517 | auth_cram_md5 = Net::SMTP::AuthCramMD5.new(smtp) 518 | auth_cram_md5.define_singleton_method(:digest_class) { raise '"openssl" or "digest" library is required' } 519 | Net::SMTP::AuthCramMD5.define_singleton_method(:new) { |_| auth_cram_md5 } 520 | e = assert_raise RuntimeError do 521 | smtp.start(user: 'account', password: 'password', authtype: :cram_md5){} 522 | end 523 | assert_equal('"openssl" or "digest" library is required', e.message) 524 | end 525 | 526 | def test_start_instance 527 | port = fake_server_start 528 | smtp = Net::SMTP.new('localhost', port) 529 | smtp.start 530 | smtp.finish 531 | end 532 | 533 | def test_start_instance_with_position_argument 534 | port = fake_server_start(auth: 'plain') 535 | smtp = Net::SMTP.new('localhost', port) 536 | smtp.start('myname', 'account', 'password', :plain) 537 | smtp.finish 538 | end 539 | 540 | def test_start_instance_with_keyword_argument 541 | port = fake_server_start(auth: 'plain') 542 | smtp = Net::SMTP.new('localhost', port) 543 | smtp.start(helo: 'myname', user: 'account', secret: 'password', authtype: :plain) 544 | smtp.finish 545 | end 546 | 547 | def test_start_instance_password_is_secret 548 | port = fake_server_start(auth: 'plain') 549 | smtp = Net::SMTP.new('localhost', port) 550 | smtp.start(helo: 'myname', user: 'account', password: 'password', authtype: :plain) 551 | smtp.finish 552 | end 553 | 554 | def test_start_instance_invalid_number_of_arguments 555 | smtp = Net::SMTP.new('localhost') 556 | err = assert_raise ArgumentError do 557 | smtp.start('myname', 'account', 'password', :plain, :invalid_arg) 558 | end 559 | assert_equal('wrong number of arguments (given 5, expected 0..4)', err.message) 560 | end 561 | 562 | def test_send_smtputf_sender_without_server 563 | server = FakeServer.start(smtputf8: false) 564 | smtp = Net::SMTP.start 'localhost', server.port 565 | smtp.send_message('message', 'rené@example.com', 'foo@example.com') 566 | assert server.commands.include? "MAIL FROM:\r\n" 567 | end 568 | 569 | def test_send_smtputf8_sender 570 | server = FakeServer.start(smtputf8: true) 571 | smtp = Net::SMTP.start 'localhost', server.port 572 | smtp.send_message('message', 'rené@example.com', 'foo@example.com') 573 | assert server.commands.include? "MAIL FROM: SMTPUTF8\r\n" 574 | end 575 | 576 | def test_send_smtputf8_sender_with_size 577 | server = FakeServer.start(smtputf8: true) 578 | smtp = Net::SMTP.start 'localhost', server.port 579 | smtp.send_message('message', Net::SMTP::Address.new('rené@example.com', 'SIZE=42'), 'foo@example.com') 580 | assert server.commands.include? "MAIL FROM: SIZE=42 SMTPUTF8\r\n" 581 | end 582 | 583 | def test_send_smtputf_recipient 584 | server = FakeServer.start(smtputf8: true) 585 | smtp = Net::SMTP.start 'localhost', server.port 586 | smtp.send_message('message', 'foo@example.com', 'rené@example.com') 587 | assert server.commands.include? "MAIL FROM: SMTPUTF8\r\n" 588 | end 589 | 590 | def test_mailfrom_with_smtputf_detection 591 | server = FakeServer.start(smtputf8: true) 592 | smtp = Net::SMTP.start 'localhost', server.port 593 | smtp.mailfrom("rené@example.com") 594 | assert_equal "MAIL FROM: SMTPUTF8\r\n", server.commands.last 595 | end 596 | 597 | def fake_server_start(**kw) 598 | server = FakeServer.new 599 | server.start(**kw) 600 | server.port 601 | end 602 | end 603 | 604 | class FakeServer 605 | CA_FILE = File.expand_path("../fixtures/cacert.pem", __dir__) 606 | SERVER_KEY = File.expand_path("../fixtures/server.key", __dir__) 607 | SERVER_CERT = File.expand_path("../fixtures/server.crt", __dir__) 608 | 609 | @servers = [] 610 | 611 | def self.start(**kw) 612 | server = self.new 613 | @servers.push server 614 | server.start(**kw) 615 | server 616 | end 617 | 618 | def self.stop_all 619 | while (s = @servers.shift) 620 | s.stop 621 | end 622 | end 623 | 624 | attr_reader :port 625 | attr_reader :commands 626 | attr_reader :body 627 | 628 | def starttls_started? 629 | !!@starttls_started 630 | end 631 | 632 | def start(**capabilities) 633 | @commands = [] 634 | @body = +'' 635 | @capa = capabilities 636 | @tls = @capa.delete(:tls) 637 | @servers = Socket.tcp_server_sockets('localhost', 0) 638 | @port = @servers[0].local_address.ip_port 639 | @server_thread = Thread.start do 640 | Thread.current.abort_on_exception = true 641 | init 642 | loop 643 | end 644 | end 645 | 646 | def stop 647 | @server_thread&.kill 648 | @server_thread&.join 649 | @servers&.each(&:close) 650 | end 651 | 652 | def init 653 | @sock = Socket.accept_loop(@servers) { |s, _| break s } 654 | if @tls 655 | @sock = ssl_socket 656 | @sock.sync_close = true 657 | @sock.accept 658 | end 659 | greeting 660 | end 661 | 662 | def ssl_socket 663 | ctx = OpenSSL::SSL::SSLContext.new 664 | ctx.ca_file = CA_FILE 665 | ctx.key = File.open(SERVER_KEY){|f| OpenSSL::PKey::RSA.new(f)} 666 | ctx.cert = File.open(SERVER_CERT){|f| OpenSSL::X509::Certificate.new(f)} 667 | OpenSSL::SSL::SSLSocket.new(@sock, ctx) 668 | end 669 | 670 | def greeting 671 | @sock.puts "220 ready\r\n" 672 | end 673 | 674 | def ehlo(_) 675 | res = [+"220-servername\r\n"] 676 | @capa.each do |k, v| 677 | case v 678 | when nil, false 679 | # do nothing 680 | when true 681 | res.push "220-#{k.upcase}\r\n" 682 | when String 683 | res.push "220-#{k.upcase} #{v.upcase}\r\n" 684 | when Array 685 | res.push "220-#{k.upcase} #{v.map(&:upcase).join(' ')}\r\n" 686 | else 687 | raise "invalid capacities: #{k}=>#{v}" 688 | end 689 | end 690 | res.last.sub!(/^220-/, '220 ') 691 | @sock.puts res.join 692 | end 693 | 694 | def starttls 695 | unless @capa[:starttls] 696 | @sock.puts "502 5.5.1 Error: command not implemented\r\n" 697 | return 698 | end 699 | @sock.puts "220 2.0.0 Ready to start TLS\r\n" 700 | @sock = ssl_socket 701 | @sock.sync_close = true 702 | @sock.accept 703 | @starttls_started = true 704 | end 705 | 706 | def auth(*args) 707 | unless @capa[:auth] 708 | @sock.puts "503 5.5.1 Error: authentication not enabled\r\n" 709 | return 710 | end 711 | type, arg = args 712 | unless Array(@capa[:auth]).map(&:upcase).include? type.upcase 713 | @sock.puts "535 5.7.8 Error: authentication failed: no mechanism available\r\n" 714 | return 715 | end 716 | # The account and password are fixed to "account" and "password". 717 | result = case type 718 | when 'PLAIN' 719 | arg == 'AGFjY291bnQAcGFzc3dvcmQ=' 720 | when 'LOGIN' 721 | @sock.puts "334 VXNlcm5hbWU6\r\n" 722 | u = @sock.gets.unpack1('m') 723 | @sock.puts "334 UGFzc3dvcmQ6\r\n" 724 | p = @sock.gets.unpack1('m') 725 | u == 'account' && p == 'password' 726 | when 'CRAM-MD5' 727 | @sock.puts "334 PDEyMzQ1Njc4OTAuMTIzNDVAc2VydmVybmFtZT4=\r\n" 728 | r = @sock.gets&.chomp 729 | r == 'YWNjb3VudCAyYzBjMTgxZjkxOGU2ZGM5Mjg3Zjk3N2E1ODhiMzg1YQ==' 730 | when 'XOAUTH2' 731 | arg == 'dXNlcj1hY2NvdW50AWF1dGg9QmVhcmVyIHRva2VuAQE=' 732 | end 733 | if result 734 | @sock.puts "235 2.7.0 Authentication successful\r\n" 735 | else 736 | @sock.puts "535 5.7.8 Error: authentication failed: authentication failure\r\n" 737 | end 738 | end 739 | 740 | def mail(_) 741 | @sock.puts "250 2.1.0 Ok\r\n" 742 | end 743 | 744 | def rcpt(_) 745 | @sock.puts "250 2.1.0 Ok\r\n" 746 | end 747 | 748 | def data 749 | @sock.puts "354 End data with .\r\n" 750 | while (l = @sock.gets) 751 | break if l.chomp == '.' 752 | @body.concat l.sub(/^\./, '') 753 | end 754 | @sock.puts "250 2.0.0 Ok: queued as ABCDEFG\r\n" 755 | end 756 | 757 | def rset 758 | @sock.puts "250 2.0.0 Ok\r\n" 759 | end 760 | 761 | def quit 762 | @sock.puts "221 2.0.0 Bye\r\n" 763 | @sock.close 764 | @servers.each(&:close) 765 | end 766 | 767 | def loop 768 | while (comm = @sock.gets) 769 | @commands.push comm.encode('utf-8', 'utf-8') 770 | case comm.chomp 771 | when /\AEHLO / 772 | ehlo(comm.split[1]) 773 | when "STARTTLS" 774 | starttls 775 | when /\AAUTH / 776 | auth(*$'.split) 777 | when /\AMAIL FROM:/ 778 | mail($') 779 | when /\ARCPT TO:/ 780 | rcpt($') 781 | when "DATA" 782 | data 783 | when "RSET" 784 | rset 785 | when "QUIT" 786 | quit 787 | break 788 | else 789 | @sock.puts "502 5.5.2 Error: command not recognized\r\n" 790 | end 791 | end 792 | rescue Errno::ECONNRESET, Errno::ECONNABORTED 793 | nil 794 | end 795 | end 796 | end 797 | -------------------------------------------------------------------------------- /lib/net/smtp.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | # = net/smtp.rb 4 | # 5 | # Copyright (c) 1999-2007 Yukihiro Matsumoto. 6 | # 7 | # Copyright (c) 1999-2007 Minero Aoki. 8 | # 9 | # Written & maintained by Minero Aoki . 10 | # 11 | # Documented by William Webber and Minero Aoki. 12 | # 13 | # This program is free software. You can re-distribute and/or 14 | # modify this program under the same terms as Ruby itself. 15 | # 16 | # See Net::SMTP for documentation. 17 | # 18 | 19 | require 'net/protocol' 20 | begin 21 | require 'openssl' 22 | rescue LoadError 23 | end 24 | 25 | module Net 26 | # Module mixed in to all SMTP error classes 27 | module SMTPError 28 | # This *class* is a module for backward compatibility. 29 | # In later release, this module becomes a class. 30 | 31 | attr_reader :response 32 | 33 | def initialize(response, message: nil) 34 | if response.is_a?(::Net::SMTP::Response) 35 | @response = response 36 | @message = message 37 | else 38 | @response = nil 39 | @message = message || response 40 | end 41 | end 42 | 43 | def message 44 | @message || response.message 45 | end 46 | end 47 | 48 | # Represents an SMTP authentication error. 49 | class SMTPAuthenticationError < ProtoAuthError 50 | include SMTPError 51 | end 52 | 53 | # Represents SMTP error code 4xx, a temporary error. 54 | class SMTPServerBusy < ProtoServerError 55 | include SMTPError 56 | end 57 | 58 | # Represents an SMTP command syntax error (error code 500) 59 | class SMTPSyntaxError < ProtoSyntaxError 60 | include SMTPError 61 | end 62 | 63 | # Represents a fatal SMTP error (error code 5xx, except for 500) 64 | class SMTPFatalError < ProtoFatalError 65 | include SMTPError 66 | end 67 | 68 | # Unexpected reply code returned from server. 69 | class SMTPUnknownError < ProtoUnknownError 70 | include SMTPError 71 | end 72 | 73 | # Command is not supported on server. 74 | class SMTPUnsupportedCommand < ProtocolError 75 | include SMTPError 76 | end 77 | 78 | # 79 | # == What is This Library? 80 | # 81 | # This library provides functionality to send internet 82 | # mail via \SMTP, the Simple Mail Transfer Protocol. For details of 83 | # \SMTP itself, see [RFC5321[https://www.rfc-editor.org/rfc/rfc5321.txt]]. 84 | # This library also implements \SMTP authentication, which is often 85 | # necessary for message composers to submit messages to their 86 | # outgoing \SMTP server, see 87 | # [RFC6409[https://www.rfc-editor.org/rfc/rfc6409.html]], 88 | # and [SMTPUTF8[https://www.rfc-editor.org/rfc/rfc6531.txt]], which is 89 | # necessary to send messages to/from addresses containing characters 90 | # outside the ASCII range. 91 | # 92 | # == What is This Library NOT? 93 | # 94 | # This library does NOT provide functions to compose internet mails. 95 | # You must create them by yourself. If you want better mail support, 96 | # try the mail[https://rubygems.org/gems/mail] or 97 | # rmail[https://rubygems.org/gems/rmail] gems, or search for alternatives in 98 | # {RubyGems.org}[https://rubygems.org/] or {The Ruby 99 | # Toolbox}[https://www.ruby-toolbox.com/]. 100 | # 101 | # FYI: the official specification on internet mail is: 102 | # [RFC5322[https://www.rfc-editor.org/rfc/rfc5322.txt]]. 103 | # 104 | # == Examples 105 | # 106 | # === Sending Messages 107 | # 108 | # You must open a connection to an \SMTP server before sending messages. 109 | # The first argument is the address of your \SMTP server, and the second 110 | # argument is the port number. Using SMTP.start with a block is the simplest 111 | # way to do this. This way, the SMTP connection is closed automatically 112 | # after the block is executed. 113 | # 114 | # require 'net/smtp' 115 | # Net::SMTP.start('your.smtp.server', 25) do |smtp| 116 | # # Use the SMTP object smtp only in this block. 117 | # end 118 | # 119 | # Replace 'your.smtp.server' with your \SMTP server. Normally 120 | # your system manager or internet provider supplies a server 121 | # for you. 122 | # 123 | # Then you can send messages. 124 | # 125 | # msgstr = < 127 | # To: Destination Address 128 | # Subject: test message 129 | # Date: Sat, 23 Jun 2001 16:26:43 +0900 130 | # Message-Id: 131 | # 132 | # This is a test message. 133 | # END_OF_MESSAGE 134 | # 135 | # require 'net/smtp' 136 | # Net::SMTP.start('your.smtp.server', 25) do |smtp| 137 | # smtp.send_message msgstr, 138 | # 'your@mail.address', 139 | # 'his_address@example.com' 140 | # end 141 | # 142 | # === Closing the Session 143 | # 144 | # You MUST close the SMTP session after sending messages, by calling 145 | # the #finish method: 146 | # 147 | # # using SMTP#finish 148 | # smtp = Net::SMTP.start('your.smtp.server', 25) 149 | # smtp.send_message msgstr, 'from@address', 'to@address' 150 | # smtp.finish 151 | # 152 | # You can also use the block form of SMTP.start or SMTP#start. This closes 153 | # the SMTP session automatically: 154 | # 155 | # # using block form of SMTP.start 156 | # Net::SMTP.start('your.smtp.server', 25) do |smtp| 157 | # smtp.send_message msgstr, 'from@address', 'to@address' 158 | # end 159 | # 160 | # I strongly recommend this scheme. This form is simpler and more robust. 161 | # 162 | # === HELO domain 163 | # 164 | # In almost all situations, you must provide a third argument 165 | # to SMTP.start or SMTP#start. This is the domain name which you are on 166 | # (the host to send mail from). It is called the "HELO domain". 167 | # The \SMTP server will judge whether it should send or reject 168 | # the SMTP session by inspecting the HELO domain. 169 | # 170 | # Net::SMTP.start('your.smtp.server', 25, helo: 'mail.from.domain') do |smtp| 171 | # smtp.send_message msgstr, 'from@address', 'to@address' 172 | # end 173 | # 174 | # === \SMTP Authentication 175 | # 176 | # The Net::SMTP class supports the \SMTP extension for SASL Authentication 177 | # [RFC4954[https://www.rfc-editor.org/rfc/rfc4954.html]] and the following 178 | # SASL mechanisms: +PLAIN+, +LOGIN+ _(deprecated)_, and +CRAM-MD5+ 179 | # _(deprecated)_. 180 | # 181 | # To use \SMTP authentication, pass extra arguments to 182 | # SMTP.start or SMTP#start. 183 | # 184 | # # PLAIN 185 | # Net::SMTP.start('your.smtp.server', 25, 186 | # user: 'Your Account', secret: 'Your Password', authtype: :plain) 187 | # 188 | # Support for other SASL mechanisms-such as +EXTERNAL+, +OAUTHBEARER+, 189 | # +SCRAM-SHA-256+, and +XOAUTH2+-will be added in a future release. 190 | # 191 | # The +LOGIN+ and +CRAM-MD5+ mechanisms are still available for backwards 192 | # compatibility, but are deprecated and should be avoided. 193 | # 194 | class SMTP < Protocol 195 | VERSION = "0.5.1" 196 | 197 | # The default SMTP port number, 25. 198 | def SMTP.default_port 199 | 25 200 | end 201 | 202 | # The default mail submission port number, 587. 203 | def SMTP.default_submission_port 204 | 587 205 | end 206 | 207 | # The default SMTPS port number, 465. 208 | def SMTP.default_tls_port 209 | 465 210 | end 211 | 212 | class << self 213 | alias default_ssl_port default_tls_port 214 | end 215 | 216 | def SMTP.default_ssl_context(ssl_context_params = nil) 217 | context = OpenSSL::SSL::SSLContext.new 218 | context.set_params(ssl_context_params || {}) 219 | context 220 | end 221 | 222 | # 223 | # Creates a new Net::SMTP object. 224 | # 225 | # +address+ is the hostname or ip address of your SMTP 226 | # server. +port+ is the port to connect to; it defaults to 227 | # port 25. 228 | # 229 | # If +tls+ is true, enable TLS. The default is false. 230 | # If +starttls+ is :always, enable STARTTLS, if +:auto+, use STARTTLS when the server supports it, 231 | # if false, disable STARTTLS. 232 | # 233 | # If +tls_verify+ is true, verify the server's certificate. The default is true. 234 | # If the hostname in the server certificate is different from +address+, 235 | # it can be specified with +tls_hostname+. 236 | # 237 | # Additional SSLContext[https://ruby.github.io/openssl/OpenSSL/SSL/SSLContext.html] 238 | # params can be added to the +ssl_context_params+ hash argument and are 239 | # passed to {OpenSSL::SSL::SSLContext#set_params}[https://ruby.github.io/openssl/OpenSSL/SSL/SSLContext.html#method-i-set_params]. 240 | # 241 | # tls_verify: true is equivalent to ssl_context_params: { 242 | # verify_mode: OpenSSL::SSL::VERIFY_PEER }. 243 | # 244 | # This method does not open the TCP connection. You can use 245 | # SMTP.start instead of SMTP.new if you want to do everything 246 | # at once. Otherwise, follow SMTP.new with SMTP#start. 247 | # 248 | def initialize(address, port = nil, tls: false, starttls: :auto, tls_verify: true, tls_hostname: nil, ssl_context_params: nil) 249 | @address = address 250 | @port = (port || SMTP.default_port) 251 | @esmtp = true 252 | @capabilities = nil 253 | @socket = nil 254 | @started = false 255 | @open_timeout = 30 256 | @read_timeout = 60 257 | @error_occurred = false 258 | @debug_output = nil 259 | @tls = tls 260 | @starttls = starttls 261 | @ssl_context_tls = nil 262 | @ssl_context_starttls = nil 263 | @tls_verify = tls_verify 264 | @tls_hostname = tls_hostname 265 | @ssl_context_params = ssl_context_params 266 | end 267 | 268 | # If +true+, verify th server's certificate. 269 | attr_accessor :tls_verify 270 | 271 | # The hostname for verifying hostname in the server certificatate. 272 | attr_accessor :tls_hostname 273 | 274 | # Hash for additional SSLContext parameters. 275 | attr_accessor :ssl_context_params 276 | 277 | # Provide human-readable stringification of class state. 278 | def inspect 279 | "#<#{self.class} #{@address}:#{@port} started=#{@started}>" 280 | end 281 | 282 | # 283 | # Set whether to use ESMTP or not. This should be done before 284 | # calling #start. Note that if #start is called in ESMTP mode, 285 | # and the connection fails due to a ProtocolError, the SMTP 286 | # object will automatically switch to plain SMTP mode and 287 | # retry (but not vice versa). 288 | # 289 | attr_accessor :esmtp 290 | 291 | # +true+ if the SMTP object uses ESMTP (which it does by default). 292 | alias esmtp? esmtp 293 | 294 | # true if server advertises STARTTLS. 295 | # You cannot get valid value before opening SMTP session. 296 | def capable_starttls? 297 | capable?('STARTTLS') 298 | end 299 | 300 | # true if the EHLO response contains +key+. 301 | def capable?(key) 302 | return nil unless @capabilities 303 | @capabilities[key] ? true : false 304 | end 305 | 306 | # The server capabilities by EHLO response 307 | attr_reader :capabilities 308 | 309 | # true if server advertises AUTH PLAIN. 310 | # You cannot get valid value before opening SMTP session. 311 | def capable_plain_auth? 312 | auth_capable?('PLAIN') 313 | end 314 | 315 | # true if server advertises AUTH LOGIN. 316 | # You cannot get valid value before opening SMTP session. 317 | def capable_login_auth? 318 | auth_capable?('LOGIN') 319 | end 320 | 321 | # true if server advertises AUTH CRAM-MD5. 322 | # You cannot get valid value before opening SMTP session. 323 | def capable_cram_md5_auth? 324 | auth_capable?('CRAM-MD5') 325 | end 326 | 327 | # Returns whether the server advertises support for the authentication type. 328 | # You cannot get valid result before opening SMTP session. 329 | def auth_capable?(type) 330 | return nil unless @capabilities 331 | return false unless @capabilities['AUTH'] 332 | @capabilities['AUTH'].include?(type) 333 | end 334 | 335 | # Returns supported authentication methods on this server. 336 | # You cannot get valid value before opening SMTP session. 337 | def capable_auth_types 338 | return [] unless @capabilities 339 | return [] unless @capabilities['AUTH'] 340 | @capabilities['AUTH'] 341 | end 342 | 343 | # true if this object uses SMTP/TLS (SMTPS). 344 | def tls? 345 | @tls 346 | end 347 | 348 | alias ssl? tls? 349 | 350 | # Enables SMTP/TLS (SMTPS: \SMTP over direct TLS connection) for 351 | # this object. Must be called before the connection is established 352 | # to have any effect. +context+ is a OpenSSL::SSL::SSLContext object. 353 | def enable_tls(context = nil) 354 | raise 'openssl library not installed' unless defined?(OpenSSL::VERSION) 355 | raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @starttls == :always 356 | @tls = true 357 | @ssl_context_tls = context 358 | end 359 | 360 | alias enable_ssl enable_tls 361 | 362 | # Disables SMTP/TLS for this object. Must be called before the 363 | # connection is established to have any effect. 364 | def disable_tls 365 | @tls = false 366 | @ssl_context_tls = nil 367 | end 368 | 369 | alias disable_ssl disable_tls 370 | 371 | # Returns truth value if this object uses STARTTLS. 372 | # If this object always uses STARTTLS, returns :always. 373 | # If this object uses STARTTLS when the server support TLS, returns :auto. 374 | def starttls? 375 | @starttls 376 | end 377 | 378 | # true if this object uses STARTTLS. 379 | def starttls_always? 380 | @starttls == :always 381 | end 382 | 383 | # true if this object uses STARTTLS when server advertises STARTTLS. 384 | def starttls_auto? 385 | @starttls == :auto 386 | end 387 | 388 | # Enables SMTP/TLS (STARTTLS) for this object. 389 | # +context+ is a OpenSSL::SSL::SSLContext object. 390 | def enable_starttls(context = nil) 391 | raise 'openssl library not installed' unless defined?(OpenSSL::VERSION) 392 | raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @tls 393 | @starttls = :always 394 | @ssl_context_starttls = context 395 | end 396 | 397 | # Enables SMTP/TLS (STARTTLS) for this object if server accepts. 398 | # +context+ is a OpenSSL::SSL::SSLContext object. 399 | def enable_starttls_auto(context = nil) 400 | raise 'openssl library not installed' unless defined?(OpenSSL::VERSION) 401 | raise ArgumentError, "SMTPS and STARTTLS is exclusive" if @tls 402 | @starttls = :auto 403 | @ssl_context_starttls = context 404 | end 405 | 406 | # Disables SMTP/TLS (STARTTLS) for this object. Must be called 407 | # before the connection is established to have any effect. 408 | def disable_starttls 409 | @starttls = false 410 | @ssl_context_starttls = nil 411 | end 412 | 413 | # The address of the SMTP server to connect to. 414 | attr_reader :address 415 | 416 | # The port number of the SMTP server to connect to. 417 | attr_reader :port 418 | 419 | # Seconds to wait while attempting to open a connection. 420 | # If the connection cannot be opened within this time, a 421 | # Net::OpenTimeout is raised. The default value is 30 seconds. 422 | attr_accessor :open_timeout 423 | 424 | # Seconds to wait while reading one block (by one read(2) call). 425 | # If the read(2) call does not complete within this time, a 426 | # Net::ReadTimeout is raised. The default value is 60 seconds. 427 | attr_reader :read_timeout 428 | 429 | # Set the number of seconds to wait until timing-out a read(2) 430 | # call. 431 | def read_timeout=(sec) 432 | @socket.read_timeout = sec if @socket 433 | @read_timeout = sec 434 | end 435 | 436 | # 437 | # WARNING: This method causes serious security holes. 438 | # Use this method for only debugging. 439 | # 440 | # Set an output stream for debug logging. 441 | # You must call this before #start. 442 | # 443 | # # example 444 | # smtp = Net::SMTP.new(addr, port) 445 | # smtp.set_debug_output $stderr 446 | # smtp.start do |smtp| 447 | # .... 448 | # end 449 | # 450 | def debug_output=(arg) 451 | @debug_output = arg 452 | end 453 | 454 | alias set_debug_output debug_output= 455 | 456 | # 457 | # SMTP session control 458 | # 459 | 460 | # 461 | # :call-seq: 462 | # start(address, port = nil, helo: 'localhost', user: nil, secret: nil, authtype: nil, tls: false, starttls: :auto, tls_verify: true, tls_hostname: nil, ssl_context_params: nil) { |smtp| ... } 463 | # start(address, port = nil, helo = 'localhost', user = nil, secret = nil, authtype = nil) { |smtp| ... } 464 | # 465 | # Creates a new Net::SMTP object and connects to the server. 466 | # 467 | # This method is equivalent to: 468 | # 469 | # Net::SMTP.new(address, port, tls_verify: flag, tls_hostname: hostname, ssl_context_params: nil) 470 | # .start(helo: helo_domain, user: account, secret: password, authtype: authtype) 471 | # 472 | # See also: Net::SMTP.new, #start 473 | # 474 | # === Example 475 | # 476 | # Net::SMTP.start('your.smtp.server') do |smtp| 477 | # smtp.send_message msgstr, 'from@example.com', ['dest@example.com'] 478 | # end 479 | # 480 | # === Block Usage 481 | # 482 | # If called with a block, the newly-opened Net::SMTP object is yielded 483 | # to the block, and automatically closed when the block finishes. If called 484 | # without a block, the newly-opened Net::SMTP object is returned to 485 | # the caller, and it is the caller's responsibility to close it when 486 | # finished. 487 | # 488 | # === Parameters 489 | # 490 | # +address+ is the hostname or ip address of your smtp server. 491 | # 492 | # +port+ is the port to connect to; it defaults to port 25. 493 | # 494 | # +helo+ is the _HELO_ _domain_ provided by the client to the 495 | # server (see overview comments); it defaults to 'localhost'. 496 | # 497 | # If +tls+ is true, enable TLS. The default is false. 498 | # If +starttls+ is :always, enable STARTTLS, if +:auto+, use STARTTLS when the server supports it, 499 | # if false, disable STARTTLS. 500 | # 501 | # If +tls_verify+ is true, verify the server's certificate. The default is true. 502 | # If the hostname in the server certificate is different from +address+, 503 | # it can be specified with +tls_hostname+. 504 | # 505 | # Additional SSLContext[https://ruby.github.io/openssl/OpenSSL/SSL/SSLContext.html] 506 | # params can be added to the +ssl_context_params+ hash argument and are 507 | # passed to {OpenSSL::SSL::SSLContext#set_params}[https://ruby.github.io/openssl/OpenSSL/SSL/SSLContext.html#method-i-set_params]. 508 | # 509 | # tls_verify: true is equivalent to ssl_context_params: { 510 | # verify_mode: OpenSSL::SSL::VERIFY_PEER }. 511 | # 512 | # The remaining arguments are used for \SMTP authentication, if required or 513 | # desired. 514 | # 515 | # +authtype+ is the SASL authentication mechanism. 516 | # 517 | # +user+ is the authentication or authorization identity. 518 | # 519 | # +secret+ or +password+ is your password or other authentication token. 520 | # 521 | # These will be sent to #authenticate as positional arguments-the exact 522 | # semantics are dependent on the +authtype+. 523 | # 524 | # See the discussion of Net::SMTP@SMTP+Authentication in the overview notes. 525 | # 526 | # === Errors 527 | # 528 | # This method may raise: 529 | # 530 | # * Net::SMTPAuthenticationError 531 | # * Net::SMTPServerBusy 532 | # * Net::SMTPSyntaxError 533 | # * Net::SMTPFatalError 534 | # * Net::SMTPUnknownError 535 | # * Net::OpenTimeout 536 | # * Net::ReadTimeout 537 | # * IOError 538 | # 539 | def SMTP.start(address, port = nil, *args, helo: nil, 540 | user: nil, secret: nil, password: nil, authtype: nil, 541 | tls: false, starttls: :auto, 542 | tls_verify: true, tls_hostname: nil, ssl_context_params: nil, 543 | &block) 544 | raise ArgumentError, "wrong number of arguments (given #{args.size + 2}, expected 1..6)" if args.size > 4 545 | helo ||= args[0] || 'localhost' 546 | user ||= args[1] 547 | secret ||= password || args[2] 548 | authtype ||= args[3] 549 | new(address, port, tls: tls, starttls: starttls, tls_verify: tls_verify, tls_hostname: tls_hostname, ssl_context_params: ssl_context_params).start(helo: helo, user: user, secret: secret, authtype: authtype, &block) 550 | end 551 | 552 | # +true+ if the \SMTP session has been started. 553 | def started? 554 | @started 555 | end 556 | 557 | # 558 | # :call-seq: 559 | # start(helo: 'localhost', user: nil, secret: nil, authtype: nil) { |smtp| ... } 560 | # start(helo = 'localhost', user = nil, secret = nil, authtype = nil) { |smtp| ... } 561 | # 562 | # Opens a TCP connection and starts the SMTP session. 563 | # 564 | # === Parameters 565 | # 566 | # +helo+ is the _HELO_ _domain_ that you'll dispatch mails from; see 567 | # the discussion in the overview notes. 568 | # 569 | # The remaining arguments are used for \SMTP authentication, if required or 570 | # desired. 571 | # 572 | # +authtype+ is the SASL authentication mechanism. 573 | # 574 | # +user+ is the authentication or authorization identity. 575 | # 576 | # +secret+ or +password+ is your password or other authentication token. 577 | # 578 | # These will be sent to #authenticate as positional arguments-the exact 579 | # semantics are dependent on the +authtype+. 580 | # 581 | # See the discussion of Net::SMTP@SMTP+Authentication in the overview notes. 582 | # 583 | # See also: Net::SMTP.start 584 | # 585 | # === Block Usage 586 | # 587 | # When this methods is called with a block, the newly-started SMTP 588 | # object is yielded to the block, and automatically closed after 589 | # the block call finishes. Otherwise, it is the caller's 590 | # responsibility to close the session when finished. 591 | # 592 | # === Example 593 | # 594 | # This is very similar to the class method SMTP.start. 595 | # 596 | # require 'net/smtp' 597 | # smtp = Net::SMTP.new('smtp.mail.server', 25) 598 | # smtp.start(helo: helo_domain, user: account, secret: password, authtype: authtype) do |smtp| 599 | # smtp.send_message msgstr, 'from@example.com', ['dest@example.com'] 600 | # end 601 | # 602 | # The primary use of this method (as opposed to SMTP.start) 603 | # is probably to set debugging (#set_debug_output) or ESMTP 604 | # (#esmtp=), which must be done before the session is 605 | # started. 606 | # 607 | # === Errors 608 | # 609 | # If session has already been started, an IOError will be raised. 610 | # 611 | # This method may raise: 612 | # 613 | # * Net::SMTPAuthenticationError 614 | # * Net::SMTPServerBusy 615 | # * Net::SMTPSyntaxError 616 | # * Net::SMTPFatalError 617 | # * Net::SMTPUnknownError 618 | # * Net::OpenTimeout 619 | # * Net::ReadTimeout 620 | # * IOError 621 | # 622 | def start(*args, helo: nil, user: nil, secret: nil, password: nil, authtype: nil) 623 | raise ArgumentError, "wrong number of arguments (given #{args.size}, expected 0..4)" if args.size > 4 624 | helo ||= args[0] || 'localhost' 625 | user ||= args[1] 626 | secret ||= password || args[2] 627 | authtype ||= args[3] 628 | if defined?(OpenSSL::VERSION) 629 | ssl_context_params = @ssl_context_params || {} 630 | unless ssl_context_params.has_key?(:verify_mode) 631 | ssl_context_params[:verify_mode] = @tls_verify ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE 632 | end 633 | if @tls && @ssl_context_tls.nil? 634 | @ssl_context_tls = SMTP.default_ssl_context(ssl_context_params) 635 | end 636 | if @starttls && @ssl_context_starttls.nil? 637 | @ssl_context_starttls = SMTP.default_ssl_context(ssl_context_params) 638 | end 639 | end 640 | if block_given? 641 | begin 642 | do_start helo, user, secret, authtype 643 | return yield(self) 644 | ensure 645 | do_finish 646 | end 647 | else 648 | do_start helo, user, secret, authtype 649 | return self 650 | end 651 | end 652 | 653 | # Finishes the SMTP session and closes TCP connection. 654 | # Raises IOError if not started. 655 | def finish 656 | raise IOError, 'not yet started' unless started? 657 | do_finish 658 | end 659 | 660 | private 661 | 662 | def tcp_socket(address, port) 663 | TCPSocket.open address, port 664 | end 665 | 666 | def do_start(helo_domain, user, secret, authtype) 667 | raise IOError, 'SMTP session already started' if @started 668 | if user || secret || authtype 669 | check_auth_args authtype, user, secret 670 | end 671 | s = Timeout.timeout(@open_timeout, Net::OpenTimeout) do 672 | tcp_socket(@address, @port) 673 | end 674 | logging "Connection opened: #{@address}:#{@port}" 675 | @socket = new_internet_message_io(tls? ? tlsconnect(s, @ssl_context_tls) : s) 676 | check_response critical { recv_response() } 677 | do_helo helo_domain 678 | if ! tls? and (starttls_always? or (capable_starttls? and starttls_auto?)) 679 | unless capable_starttls? 680 | raise SMTPUnsupportedCommand, "STARTTLS is not supported on this server" 681 | end 682 | starttls 683 | @socket = new_internet_message_io(tlsconnect(s, @ssl_context_starttls)) 684 | # helo response may be different after STARTTLS 685 | do_helo helo_domain 686 | end 687 | authenticate user, secret, (authtype || DEFAULT_AUTH_TYPE) if user 688 | @started = true 689 | ensure 690 | unless @started 691 | # authentication failed, cancel connection. 692 | s.close if s 693 | @socket = nil 694 | end 695 | end 696 | 697 | def ssl_socket(socket, context) 698 | OpenSSL::SSL::SSLSocket.new socket, context 699 | end 700 | 701 | def tlsconnect(s, context) 702 | verified = false 703 | s = ssl_socket(s, context) 704 | logging "TLS connection started" 705 | s.sync_close = true 706 | s.hostname = @tls_hostname || @address 707 | ssl_socket_connect(s, @open_timeout) 708 | verified = true 709 | s 710 | ensure 711 | s.close unless verified 712 | end 713 | 714 | def new_internet_message_io(s) 715 | InternetMessageIO.new(s, read_timeout: @read_timeout, 716 | debug_output: @debug_output) 717 | end 718 | 719 | def do_helo(helo_domain) 720 | res = @esmtp ? ehlo(helo_domain) : helo(helo_domain) 721 | @capabilities = res.capabilities 722 | rescue SMTPError 723 | if @esmtp 724 | @esmtp = false 725 | @error_occurred = false 726 | retry 727 | end 728 | raise 729 | end 730 | 731 | def do_finish 732 | quit if @socket and not @socket.closed? and not @error_occurred 733 | ensure 734 | @started = false 735 | @error_occurred = false 736 | @socket.close if @socket 737 | @socket = nil 738 | end 739 | 740 | def requires_smtputf8(address) 741 | if address.kind_of? Address 742 | !address.address.ascii_only? 743 | else 744 | !address.ascii_only? 745 | end 746 | end 747 | 748 | def any_require_smtputf8(addresses) 749 | addresses.any?{ |a| requires_smtputf8(a) } 750 | end 751 | 752 | # 753 | # Message Sending 754 | # 755 | 756 | public 757 | 758 | # 759 | # Sends +msgstr+ as a message. Single CR ("\r") and LF ("\n") found 760 | # in the +msgstr+, are converted into the CR LF pair. You cannot send a 761 | # binary message with this method. +msgstr+ should include both 762 | # the message headers and body. 763 | # 764 | # +from_addr+ is a String or Net::SMTP::Address representing the source mail address. 765 | # 766 | # +to_addr+ is a String or Net::SMTP::Address or Array of them, representing 767 | # the destination mail address or addresses. 768 | # 769 | # === Example 770 | # 771 | # Net::SMTP.start('smtp.example.com') do |smtp| 772 | # smtp.send_message msgstr, 773 | # 'from@example.com', 774 | # ['dest@example.com', 'dest2@example.com'] 775 | # end 776 | # 777 | # Net::SMTP.start('smtp.example.com') do |smtp| 778 | # smtp.send_message msgstr, 779 | # Net::SMTP::Address.new('from@example.com', size: 12345), 780 | # Net::SMTP::Address.new('dest@example.com', notify: :success) 781 | # end 782 | # 783 | # === Errors 784 | # 785 | # This method may raise: 786 | # 787 | # * Net::SMTPServerBusy 788 | # * Net::SMTPSyntaxError 789 | # * Net::SMTPFatalError 790 | # * Net::SMTPUnknownError 791 | # * Net::ReadTimeout 792 | # * IOError 793 | # 794 | def send_message(msgstr, from_addr, *to_addrs) 795 | to_addrs.flatten! 796 | raise IOError, 'closed session' unless @socket 797 | from_addr = Address.new(from_addr, 'SMTPUTF8') if any_require_smtputf8(to_addrs) && capable?('SMTPUTF8') 798 | mailfrom from_addr 799 | rcptto_list(to_addrs) {data msgstr} 800 | end 801 | 802 | alias send_mail send_message 803 | alias sendmail send_message # obsolete 804 | 805 | # 806 | # Opens a message writer stream and gives it to the block. 807 | # The stream is valid only in the block, and has these methods: 808 | # 809 | # puts(str = ''):: outputs STR and CR LF. 810 | # print(str):: outputs STR. 811 | # printf(fmt, *args):: outputs sprintf(fmt,*args). 812 | # write(str):: outputs STR and returns the length of written bytes. 813 | # <<(str):: outputs STR and returns self. 814 | # 815 | # If a single CR ("\r") or LF ("\n") is found in the message, 816 | # it is converted to the CR LF pair. You cannot send a binary 817 | # message with this method. 818 | # 819 | # === Parameters 820 | # 821 | # +from_addr+ is a String or Net::SMTP::Address representing the source mail address. 822 | # 823 | # +to_addr+ is a String or Net::SMTP::Address or Array of them, representing 824 | # the destination mail address or addresses. 825 | # 826 | # === Example 827 | # 828 | # Net::SMTP.start('smtp.example.com', 25) do |smtp| 829 | # smtp.open_message_stream('from@example.com', ['dest@example.com']) do |f| 830 | # f.puts 'From: from@example.com' 831 | # f.puts 'To: dest@example.com' 832 | # f.puts 'Subject: test message' 833 | # f.puts 834 | # f.puts 'This is a test message.' 835 | # end 836 | # end 837 | # 838 | # === Errors 839 | # 840 | # This method may raise: 841 | # 842 | # * Net::SMTPServerBusy 843 | # * Net::SMTPSyntaxError 844 | # * Net::SMTPFatalError 845 | # * Net::SMTPUnknownError 846 | # * Net::ReadTimeout 847 | # * IOError 848 | # 849 | def open_message_stream(from_addr, *to_addrs, &block) # :yield: stream 850 | to_addrs.flatten! 851 | raise IOError, 'closed session' unless @socket 852 | from_addr = Address.new(from_addr, 'SMTPUTF8') if any_require_smtputf8(to_addrs) && capable?('SMTPUTF8') 853 | mailfrom from_addr 854 | rcptto_list(to_addrs) {data(&block)} 855 | end 856 | 857 | alias ready open_message_stream # obsolete 858 | 859 | # 860 | # Authentication 861 | # 862 | 863 | DEFAULT_AUTH_TYPE = :plain 864 | 865 | # Authenticates with the server, using the "AUTH" command. 866 | # 867 | # +authtype+ is the name of a SASL authentication mechanism. 868 | # 869 | # All arguments-other than +authtype+-are forwarded to the authenticator. 870 | # Different authenticators may interpret the +user+ and +secret+ 871 | # arguments differently. 872 | def authenticate(user, secret, authtype = DEFAULT_AUTH_TYPE) 873 | check_auth_args authtype, user, secret 874 | authenticator = Authenticator.auth_class(authtype).new(self) 875 | authenticator.auth(user, secret) 876 | end 877 | 878 | private 879 | 880 | def check_auth_args(type, *args, **kwargs) 881 | type ||= DEFAULT_AUTH_TYPE 882 | klass = Authenticator.auth_class(type) or 883 | raise ArgumentError, "wrong authentication type #{type}" 884 | klass.check_args(*args, **kwargs) 885 | end 886 | 887 | # 888 | # SMTP command dispatcher 889 | # 890 | 891 | public 892 | 893 | # Aborts the current mail transaction 894 | 895 | def rset 896 | getok('RSET') 897 | end 898 | 899 | def starttls 900 | getok('STARTTLS') 901 | end 902 | 903 | def helo(domain) 904 | getok("HELO #{domain}") 905 | end 906 | 907 | def ehlo(domain) 908 | getok("EHLO #{domain}") 909 | end 910 | 911 | # +from_addr+ is +String+ or +Net::SMTP::Address+ 912 | def mailfrom(from_addr) 913 | addr = if requires_smtputf8(from_addr) && capable?("SMTPUTF8") 914 | Address.new(from_addr, "SMTPUTF8") 915 | else 916 | Address.new(from_addr) 917 | end 918 | getok((["MAIL FROM:<#{addr.address}>"] + addr.parameters).join(' ')) 919 | end 920 | 921 | def rcptto_list(to_addrs) 922 | raise ArgumentError, 'mail destination not given' if to_addrs.empty? 923 | to_addrs.flatten.each do |addr| 924 | rcptto addr 925 | end 926 | yield 927 | end 928 | 929 | # +to_addr+ is +String+ or +Net::SMTP::Address+ 930 | def rcptto(to_addr) 931 | addr = Address.new(to_addr) 932 | getok((["RCPT TO:<#{addr.address}>"] + addr.parameters).join(' ')) 933 | end 934 | 935 | # This method sends a message. 936 | # If +msgstr+ is given, sends it as a message. 937 | # If block is given, yield a message writer stream. 938 | # You must write message before the block is closed. 939 | # 940 | # # Example 1 (by string) 941 | # smtp.data(<] 1134 | attr_reader :parameters 1135 | 1136 | # :call-seq: 1137 | # initialize(address, parameter, ...) 1138 | # 1139 | # address +String+ or +Net::SMTP::Address+ 1140 | # parameter +String+ or +Hash+ 1141 | def initialize(address, *args, **kw_args) 1142 | if address.kind_of? Address 1143 | @address = address.address 1144 | @parameters = address.parameters 1145 | else 1146 | @address = address 1147 | @parameters = [] 1148 | end 1149 | @parameters = (parameters + args + [kw_args]).map{|param| Array(param)}.flatten(1).map{|param| Array(param).compact.join('=')}.uniq 1150 | end 1151 | 1152 | def to_s 1153 | @address 1154 | end 1155 | end 1156 | end # class SMTP 1157 | 1158 | SMTPSession = SMTP # :nodoc: 1159 | end 1160 | 1161 | require_relative 'smtp/authenticator' 1162 | Dir.glob("#{__dir__}/smtp/auth_*.rb") do |r| 1163 | require_relative r 1164 | end 1165 | --------------------------------------------------------------------------------