chef 0.9.8.rc.0 → 0.9.8
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +80 -44
- data/lib/chef/checksum.rb +7 -4
- data/lib/chef/client.rb +4 -0
- data/lib/chef/cookbook/cookbook_collection.rb +10 -9
- data/lib/chef/cookbook/file_system_file_vendor.rb +9 -8
- data/lib/chef/cookbook/file_vendor.rb +2 -1
- data/lib/chef/cookbook/metadata.rb +3 -0
- data/lib/chef/cookbook/remote_file_vendor.rb +3 -2
- data/lib/chef/cookbook/syntax_check.rb +8 -0
- data/lib/chef/cookbook_site_streaming_uploader.rb +5 -1
- data/lib/chef/cookbook_version.rb +25 -19
- data/lib/chef/exceptions.rb +3 -0
- data/lib/chef/file_access_control.rb +1 -0
- data/lib/chef/handler.rb +37 -1
- data/lib/chef/mixin/deep_merge.rb +6 -5
- data/lib/chef/mixin/recipe_definition_dsl_core.rb +2 -1
- data/lib/chef/mixin/shell_out.rb +1 -1
- data/lib/chef/mixin/template.rb +1 -1
- data/lib/chef/mixin/xml_escape.rb +2 -2
- data/lib/chef/monkey_patches/dir.rb +1 -1
- data/lib/chef/monkey_patches/string.rb +2 -1
- data/lib/chef/monkey_patches/tempfile.rb +5 -1
- data/lib/chef/provider/package/easy_install.rb +29 -22
- data/lib/chef/provider/template.rb +1 -94
- data/lib/chef/recipe.rb +16 -12
- data/lib/chef/rest.rb +17 -5
- data/lib/chef/run_context.rb +2 -1
- data/lib/chef/run_status.rb +27 -0
- data/lib/chef/runner.rb +5 -3
- data/lib/chef/shef.rb +3 -1
- data/lib/chef/shef/ext.rb +1 -0
- data/lib/chef/shef/model_wrapper.rb +1 -1
- data/lib/chef/shef/shef_rest.rb +1 -1
- data/lib/chef/shef/shef_session.rb +1 -0
- data/lib/chef/shell_out.rb +47 -19
- data/lib/chef/version.rb +1 -1
- metadata +6 -27
data/README.rdoc
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
= Chef
|
2
2
|
|
3
|
-
|
3
|
+
== DESCRIPTION:
|
4
4
|
|
5
5
|
Chef is a configuration management tool designed to bring automation to your entire infrastructure.
|
6
6
|
|
@@ -12,7 +12,7 @@ This README focuses on developers who want to modify Chef source code. For users
|
|
12
12
|
|
13
13
|
* http://wiki.opscode.com/display/chef/Installing+Chef+from+HEAD
|
14
14
|
|
15
|
-
|
15
|
+
== DEVELOPMENT:
|
16
16
|
|
17
17
|
Before working on the code, if you plan to contribute your changes, you need to read the Opscode Contributing document.
|
18
18
|
|
@@ -24,84 +24,120 @@ You will also need to set up the repository with the appropriate branches. We do
|
|
24
24
|
|
25
25
|
Once your repository is set up, you can start working on the code. We do use BDD/TDD with RSpec and Cucumber, so you'll need to get a development environment running.
|
26
26
|
|
27
|
-
|
27
|
+
== ENVIRONMENT:
|
28
28
|
|
29
29
|
In order to have a development environment where changes to the Chef code can be tested, we'll need to install a few things after setting up the Git repository.
|
30
30
|
|
31
|
-
|
31
|
+
=== Non-Gem Dependencies
|
32
32
|
|
33
33
|
Install these via your platform's preferred method; for example apt, yum, ports, emerge, etc.
|
34
34
|
|
35
|
-
* Git
|
36
|
-
*
|
37
|
-
*
|
38
|
-
*
|
39
|
-
|
40
|
-
|
35
|
+
* Git[http://git-scm.com/]
|
36
|
+
* Erlang/OTP[http://www.erlang.org/]
|
37
|
+
* CouchDB[http://couchdb.apache.org/]
|
38
|
+
* RabbitMQ[http://www.rabbitmq.com/]
|
39
|
+
* GCC and C Standard Libraries, header files, etc. (i.e., build-essential on debian/ubuntu)
|
40
|
+
* Ruby development package
|
41
|
+
* libxml2 development package
|
41
42
|
|
43
|
+
=== Runtime Rubygem Dependencies
|
44
|
+
==== Chef Client and Solo
|
42
45
|
* ohai
|
43
|
-
*
|
44
|
-
*
|
45
|
-
*
|
46
|
-
*
|
46
|
+
* bunny
|
47
|
+
* erubis
|
48
|
+
* extlib
|
49
|
+
* highline
|
50
|
+
* json (1.4.2)
|
51
|
+
* mixlib-authentication
|
52
|
+
* mixlib-cli
|
53
|
+
* mixlib-config
|
54
|
+
* mixlib-log
|
55
|
+
* moneta
|
56
|
+
* rest-client
|
57
|
+
* uuidtools
|
47
58
|
* merb-core
|
48
|
-
* roman-merb_cucumber
|
49
|
-
* thin
|
50
59
|
|
51
|
-
|
60
|
+
==== Chef Server, WebUI and Solr
|
61
|
+
All of the above, plus the following:
|
62
|
+
* coderay
|
63
|
+
* haml
|
64
|
+
* libxml-ruby
|
65
|
+
* merb-assets
|
66
|
+
* merb-core
|
67
|
+
* merb-haml
|
68
|
+
* merb-helpers
|
69
|
+
* merb-param-protection
|
70
|
+
* ruby-openid
|
71
|
+
* thin
|
52
72
|
|
53
|
-
|
73
|
+
=== Development Rubygem Dependencies
|
74
|
+
* rake[http://rake.rubyforge.org/]
|
75
|
+
* rspec[http://rspec.info/]
|
76
|
+
* cucumber[http://cukes.info/]
|
54
77
|
|
55
|
-
|
78
|
+
Ohai is also by Opscode and available on GitHub, http://github.com/opscode/ohai/tree/master.
|
56
79
|
|
57
80
|
== Starting the Environment:
|
58
81
|
|
59
|
-
|
82
|
+
=== On Mac OS X:
|
83
|
+
For ease of debugging, Chef includes a script to start each of the required
|
84
|
+
daemons in a separate Terminal.app tab via applescript:
|
60
85
|
|
61
|
-
|
86
|
+
scripts/mac-dev-start features
|
62
87
|
|
63
|
-
|
88
|
+
=== On Linux and BSD
|
64
89
|
|
65
|
-
|
66
|
-
|
67
|
-
* Starts solr
|
68
|
-
* Starts chef-indexer.
|
69
|
-
* Starts chef-server on port 4000
|
90
|
+
run the dev:features rake task. You may need to run it as root depending on how
|
91
|
+
your system is configured.
|
70
92
|
|
93
|
+
rake dev:features
|
94
|
+
|
95
|
+
=== Daemons
|
96
|
+
After starting the environment, you should have the following processes running:
|
97
|
+
* couchdb listening on port 5984
|
98
|
+
* rabbitmq listening on port 5672
|
99
|
+
* solr listening on port 8983
|
100
|
+
* chef-solr-indexer connected as a client to rabbitmq
|
101
|
+
* chef-server listening on port 4000
|
102
|
+
* chef-server-webui listening on port 4040
|
71
103
|
|
72
104
|
You'll know its running when you see:
|
73
105
|
|
74
|
-
~
|
75
|
-
merb : worker (port 4000) ~
|
76
|
-
merb : worker (port 4000) ~
|
77
|
-
merb : worker (port 4000) ~ Successfully bound to port 4000
|
106
|
+
merb : chef-server (api) : worker (port 4000) ~ Starting Thin at port 4000
|
107
|
+
merb : chef-server (api) : worker (port 4000) ~ Using Thin adapter on host 0.0.0.0 and port 4000.
|
108
|
+
merb : chef-server (api) : worker (port 4000) ~ Successfully bound to port 4000
|
78
109
|
|
79
110
|
You'll want to leave this terminal running the dev environment.
|
80
111
|
|
81
|
-
|
112
|
+
=== Web Interface:
|
82
113
|
|
83
|
-
With the dev environment running, you can now access the web interface via http://localhost:
|
114
|
+
With the dev environment running, you can now access the web interface via http://localhost:4040/.
|
84
115
|
|
85
116
|
== Spec testing:
|
86
117
|
|
87
|
-
We use RSpec for unit/spec tests.
|
118
|
+
We use RSpec for unit/spec tests. It is not necessary to start the development
|
119
|
+
environment to run the specs--they are completely standalone.
|
88
120
|
|
89
121
|
rake spec
|
90
122
|
|
91
|
-
This doesn't actually use the development environment, because it does the testing on all the Chef internals. For integration/usage testing, we use Cucumber features.
|
92
|
-
|
93
123
|
== Integration testing:
|
94
124
|
|
95
|
-
We test integration with Cucumber.
|
125
|
+
We test integration with Cucumber. To run the full suite, run the rake task:
|
126
|
+
|
127
|
+
rake features
|
128
|
+
|
129
|
+
Subsets of the integration tests can be run with the various tasks in the
|
130
|
+
features namespace. To see the full list, run
|
131
|
+
|
132
|
+
rake -T
|
133
|
+
|
134
|
+
To run individual feature tests, you can take advantage of cucumber's tagging
|
135
|
+
support. Tag the feature you wish to run (tags are denoted with a leading `@'
|
136
|
+
sign), then use the cucumber command:
|
96
137
|
|
97
|
-
|
98
|
-
rake features:api # Run Features with Cucumber
|
99
|
-
rake features:client # Run Features with Cucumber
|
100
|
-
rake features:provider:package:macports # Run Features with Cucumber
|
101
|
-
rake features:provider:remote_file # Run Features with Cucumber
|
102
|
-
rake features:search # Run Features with Cucumber
|
138
|
+
cucumber -t @my_tag
|
103
139
|
|
104
|
-
|
140
|
+
== LINKS:
|
105
141
|
|
106
142
|
Source:
|
107
143
|
|
data/lib/chef/checksum.rb
CHANGED
@@ -18,10 +18,10 @@
|
|
18
18
|
require 'chef/log'
|
19
19
|
require 'uuidtools'
|
20
20
|
|
21
|
-
|
22
|
-
# Checksum for an individual file; e.g., used for sandbox/cookbook uploading
|
23
|
-
# to track which files the system already manages.
|
24
21
|
class Chef
|
22
|
+
# == Chef::Checksum
|
23
|
+
# Checksum for an individual file; e.g., used for sandbox/cookbook uploading
|
24
|
+
# to track which files the system already manages.
|
25
25
|
class Checksum
|
26
26
|
attr_accessor :checksum, :create_time
|
27
27
|
attr_accessor :couchdb_id, :couchdb_rev
|
@@ -42,7 +42,10 @@ class Chef
|
|
42
42
|
}
|
43
43
|
}
|
44
44
|
|
45
|
-
# Creates a new Chef::Checksum object.
|
45
|
+
# Creates a new Chef::Checksum object.
|
46
|
+
# === Arguments
|
47
|
+
# checksum::: the MD5 content hash of the file
|
48
|
+
# couchdb::: An instance of Chef::CouchDB
|
46
49
|
#
|
47
50
|
# === Returns
|
48
51
|
# object<Chef::Checksum>:: Duh. :)
|
data/lib/chef/client.rb
CHANGED
@@ -36,12 +36,16 @@ require 'chef/version'
|
|
36
36
|
require 'ohai'
|
37
37
|
|
38
38
|
class Chef
|
39
|
+
# == Chef::Client
|
40
|
+
# The main object in a Chef run. Preps a Chef::Node and Chef::RunContext,
|
41
|
+
# syncs cookbooks if necessary, and triggers convergence.
|
39
42
|
class Client
|
40
43
|
attr_accessor :node
|
41
44
|
attr_accessor :ohai
|
42
45
|
attr_accessor :rest
|
43
46
|
attr_accessor :runner
|
44
47
|
|
48
|
+
#--
|
45
49
|
# TODO: timh/cw: 5-19-2010: json_attribs should be moved to RunContext?
|
46
50
|
attr_reader :json_attribs
|
47
51
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
#--
|
2
2
|
# Author:: Tim Hinderliter (<tim@opscode.com>)
|
3
3
|
# Author:: Christopher Walters (<cw@opscode.com>)
|
4
4
|
# Copyright:: Copyright (c) 2010 Opscode, Inc.
|
@@ -19,15 +19,16 @@
|
|
19
19
|
|
20
20
|
require 'extlib'
|
21
21
|
|
22
|
-
# This class is the consistent interface for a node to obtain its
|
23
|
-
# cookbooks by name.
|
24
|
-
#
|
25
|
-
# This class is basically a glorified Hash, but since there are
|
26
|
-
# several ways this cookbook information is collected,
|
27
|
-
# (e.g. CookbookLoader for solo, hash of auto-vivified Cookbook
|
28
|
-
# objects for lazily-loaded remote cookbooks), it gets transformed
|
29
|
-
# into this.
|
30
22
|
class Chef
|
23
|
+
# == Chef::CookbookCollection
|
24
|
+
# This class is the consistent interface for a node to obtain its
|
25
|
+
# cookbooks by name.
|
26
|
+
#
|
27
|
+
# This class is basically a glorified Hash, but since there are
|
28
|
+
# several ways this cookbook information is collected,
|
29
|
+
# (e.g. CookbookLoader for solo, hash of auto-vivified Cookbook
|
30
|
+
# objects for lazily-loaded remote cookbooks), it gets transformed
|
31
|
+
# into this.
|
31
32
|
class CookbookCollection < Mash
|
32
33
|
|
33
34
|
# The input is a mapping of cookbook name to CookbookVersion objects. We
|
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
#--
|
2
2
|
# Author:: Christopher Walters (<cw@opscode.com>)
|
3
3
|
# Author:: Tim Hinderliter (<tim@opscode.com>)
|
4
4
|
# Copyright:: Copyright (c) 2010 Opscode, Inc.
|
@@ -19,15 +19,16 @@
|
|
19
19
|
|
20
20
|
require 'chef/cookbook/file_vendor'
|
21
21
|
|
22
|
-
# This FileVendor loads files from Chef::Config.cookbook_path. The
|
23
|
-
# thing that's sort of janky about this FileVendor implementation is
|
24
|
-
# that it basically takes only the cookbook's name from the manifest
|
25
|
-
# and throws the rest away then re-builds the list of files on the
|
26
|
-
# disk. This is due to the manifest not having the on-disk file
|
27
|
-
# locations, since in the chef-client case, that information is
|
28
|
-
# non-sensical.
|
29
22
|
class Chef
|
30
23
|
class Cookbook
|
24
|
+
# == Chef::Cookbook::FileSystemFileVendor
|
25
|
+
# This FileVendor loads files from Chef::Config.cookbook_path. The
|
26
|
+
# thing that's sort of janky about this FileVendor implementation is
|
27
|
+
# that it basically takes only the cookbook's name from the manifest
|
28
|
+
# and throws the rest away then re-builds the list of files on the
|
29
|
+
# disk. This is due to the manifest not having the on-disk file
|
30
|
+
# locations, since in the chef-client case, that information is
|
31
|
+
# non-sensical.
|
31
32
|
class FileSystemFileVendor < FileVendor
|
32
33
|
|
33
34
|
def initialize(manifest)
|
@@ -18,9 +18,10 @@
|
|
18
18
|
#
|
19
19
|
|
20
20
|
|
21
|
-
# This class handles fetching of cookbook files based on specificity.
|
22
21
|
class Chef
|
23
22
|
class Cookbook
|
23
|
+
# == Chef::Cookbook::FileVendor
|
24
|
+
# This class handles fetching of cookbook files based on specificity.
|
24
25
|
class FileVendor
|
25
26
|
|
26
27
|
def self.on_create(&block)
|
@@ -25,6 +25,9 @@ require 'chef/cookbook/metadata/version'
|
|
25
25
|
|
26
26
|
class Chef
|
27
27
|
class Cookbook
|
28
|
+
# == Chef::Cookbook::Metadata
|
29
|
+
# Chef::Cookbook::Metadata provides a convenient DSL for declaring metadata
|
30
|
+
# about Chef Cookbooks.
|
28
31
|
class Metadata
|
29
32
|
|
30
33
|
COMPARISON_FIELDS = [ :name, :description, :long_description, :maintainer,
|
@@ -18,10 +18,11 @@
|
|
18
18
|
|
19
19
|
require 'chef/cookbook/file_vendor'
|
20
20
|
|
21
|
-
# This FileVendor loads files by either fetching them from the local cache, or
|
22
|
-
# if not available, loading them from the remote server.
|
23
21
|
class Chef
|
24
22
|
class Cookbook
|
23
|
+
# == Chef::Cookbook::RemoteFileVendor
|
24
|
+
# This FileVendor loads files by either fetching them from the local cache, or
|
25
|
+
# if not available, loading them from the remote server.
|
25
26
|
class RemoteFileVendor < FileVendor
|
26
27
|
|
27
28
|
def initialize(manifest, rest, valid_cache_entries)
|
@@ -21,11 +21,16 @@ require 'chef/mixin/shell_out'
|
|
21
21
|
|
22
22
|
class Chef
|
23
23
|
class Cookbook
|
24
|
+
# == Chef::Cookbook::SyntaxCheck
|
25
|
+
# Encapsulates the process of validating the ruby syntax of files in Chef
|
26
|
+
# cookbooks.
|
24
27
|
class SyntaxCheck
|
25
28
|
include Chef::Mixin::ShellOut
|
26
29
|
|
27
30
|
attr_reader :cookbook_path
|
28
31
|
|
32
|
+
# Creates a new SyntaxCheck given the +cookbook_name+ and a +cookbook_path+.
|
33
|
+
# If no +cookbook_path+ is given, +Chef::Config.cookbook_path+ is used.
|
29
34
|
def self.for_cookbook(cookbook_name, cookbook_path=nil)
|
30
35
|
cookbook_path ||= Chef::Config.cookbook_path
|
31
36
|
unless cookbook_path
|
@@ -34,6 +39,9 @@ class Chef
|
|
34
39
|
new(File.join(cookbook_path, cookbook_name.to_s))
|
35
40
|
end
|
36
41
|
|
42
|
+
# Create a new SyntaxCheck object
|
43
|
+
# === Arguments
|
44
|
+
# cookbook_path::: the (on disk) path to the cookbook
|
37
45
|
def initialize(cookbook_path)
|
38
46
|
@cookbook_path = cookbook_path
|
39
47
|
end
|
@@ -22,8 +22,12 @@ require 'net/http'
|
|
22
22
|
require 'mixlib/authentication/signedheaderauth'
|
23
23
|
require 'openssl'
|
24
24
|
|
25
|
-
# inspired by from http://stanislavvitvitskiy.blogspot.com/2008/12/multipart-post-in-ruby.html
|
26
25
|
class Chef
|
26
|
+
# == Chef::CookbookSiteStreamingUploader
|
27
|
+
# A streaming multipart HTTP upload implementation. Used to upload cookbooks
|
28
|
+
# (in tarball form) to http://cookbooks.opscode.com
|
29
|
+
#
|
30
|
+
# inspired by http://stanislavvitvitskiy.blogspot.com/2008/12/multipart-post-in-ruby.html
|
27
31
|
class CookbookSiteStreamingUploader
|
28
32
|
|
29
33
|
DefaultHeaders = { 'accept' => 'application/json', 'x-chef-version' => ::Chef::VERSION }
|
@@ -24,9 +24,15 @@ require 'chef/resource_definition_list'
|
|
24
24
|
require 'chef/recipe'
|
25
25
|
require 'chef/cookbook/file_vendor'
|
26
26
|
|
27
|
-
# TODO: timh/cw: 5-24-2010: mutators for files (e.g., recipe_filenames=,
|
28
|
-
# recipe_filenames.insert) should dirty the manifest so it gets regenerated.
|
29
27
|
class Chef
|
28
|
+
# == Chef::CookbookVersion
|
29
|
+
# CookbookVersion is a model object encapsulating the data about a Chef
|
30
|
+
# cookbook. Chef supports maintaining multiple versions of a cookbook on a
|
31
|
+
# single server; each version is represented by a distinct instance of this
|
32
|
+
# class.
|
33
|
+
#--
|
34
|
+
# TODO: timh/cw: 5-24-2010: mutators for files (e.g., recipe_filenames=,
|
35
|
+
# recipe_filenames.insert) should dirty the manifest so it gets regenerated.
|
30
36
|
class CookbookVersion
|
31
37
|
include Chef::IndexQueue::Indexable
|
32
38
|
|
@@ -228,23 +234,23 @@ class Chef
|
|
228
234
|
# as well as describing cookbook metadata. The manifest follows a form
|
229
235
|
# like the following:
|
230
236
|
#
|
231
|
-
#
|
232
|
-
#
|
233
|
-
#
|
234
|
-
#
|
235
|
-
#
|
236
|
-
#
|
237
|
-
#
|
238
|
-
#
|
239
|
-
#
|
240
|
-
#
|
241
|
-
#
|
242
|
-
#
|
243
|
-
#
|
244
|
-
#
|
245
|
-
#
|
246
|
-
#
|
247
|
-
#
|
237
|
+
# {
|
238
|
+
# :cookbook_name = "apache2",
|
239
|
+
# :version = "1.0",
|
240
|
+
# :name = "Apache 2"
|
241
|
+
# :metadata = ???TODO: timh/cw: 5-24-2010: describe this format,
|
242
|
+
#
|
243
|
+
# :files => [
|
244
|
+
# {
|
245
|
+
# :name => "afile.rb",
|
246
|
+
# :path => "files/ubuntu-9.10/afile.rb",
|
247
|
+
# :checksum => "2222",
|
248
|
+
# :specificity => "ubuntu-9.10"
|
249
|
+
# },
|
250
|
+
# ],
|
251
|
+
# :templates => [ manifest_record1, ... ],
|
252
|
+
# ...
|
253
|
+
# }
|
248
254
|
def manifest
|
249
255
|
unless @manifest
|
250
256
|
generate_manifest
|
data/lib/chef/exceptions.rb
CHANGED
@@ -16,6 +16,9 @@
|
|
16
16
|
# limitations under the License.
|
17
17
|
|
18
18
|
class Chef
|
19
|
+
# == Chef::Exceptions
|
20
|
+
# Chef's custom exceptions are all contained within the Chef::Exceptions
|
21
|
+
# namespace.
|
19
22
|
class Exceptions
|
20
23
|
class Application < RuntimeError; end
|
21
24
|
class Cron < RuntimeError; end
|
data/lib/chef/handler.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
|
1
|
+
#--
|
2
2
|
# Author:: Adam Jacob (<adam@opscode.com>)
|
3
3
|
# Copyright:: Copyright (c) 2010 Opscode, Inc.
|
4
4
|
# License:: Apache License, Version 2.0
|
@@ -18,6 +18,42 @@
|
|
18
18
|
require 'forwardable'
|
19
19
|
|
20
20
|
class Chef
|
21
|
+
# == Chef::Handler
|
22
|
+
# The base class for an Exception or Notification Handler. Create your own
|
23
|
+
# handler by subclassing Chef::Handler. When a Chef run fails with an
|
24
|
+
# uncaught Exception, Chef will set the +run_status+ on your handler and call
|
25
|
+
# +report+
|
26
|
+
#
|
27
|
+
# ===Example:
|
28
|
+
#
|
29
|
+
# require 'net/smtp'
|
30
|
+
#
|
31
|
+
# module MyOrg
|
32
|
+
# class OhNoes < Chef::Handler
|
33
|
+
#
|
34
|
+
# def report
|
35
|
+
# # Create the email message
|
36
|
+
# message = "From: Your Name <your@mail.address>\n"
|
37
|
+
# message << "To: Destination Address <someone@example.com>\n"
|
38
|
+
# message << "Subject: Chef Run Failure\n"
|
39
|
+
# message << "Date: #{Time.now.rfc2822}\n\n"
|
40
|
+
#
|
41
|
+
# # The Node is available as +node+
|
42
|
+
# message << "Chef run failed on #{node.name}\n"
|
43
|
+
# # +run_status+ is a value object with all of the run status data
|
44
|
+
# message << "#{run_status.formatted_exception}\n"
|
45
|
+
# # Join the backtrace lines. Coerce to an array just in case.
|
46
|
+
# message << Array(backtrace).join("\n")
|
47
|
+
#
|
48
|
+
# # Send the email
|
49
|
+
# Net::SMTP.start('your.smtp.server', 25) do |smtp|
|
50
|
+
# smtp.send_message message, 'from@address', 'to@address'
|
51
|
+
# end
|
52
|
+
# end
|
53
|
+
#
|
54
|
+
# end
|
55
|
+
# end
|
56
|
+
#
|
21
57
|
class Handler
|
22
58
|
|
23
59
|
extend Forwardable
|