ffi-ssdeep 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +3 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +27 -0
- data/Rakefile +31 -0
- data/ffi-ssdeep-0.1.0.gem +0 -0
- data/lib/ssdeep.rb +73 -0
- data/lib/ssdeep/fuzzy_hash.rb +88 -0
- data/spec/fuzzy_hash_spec.rb +50 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +23 -0
- data/spec/ssdeep_spec.rb +69 -0
- data/tasks/ann.rake +80 -0
- data/tasks/doc.rake +69 -0
- data/tasks/gem.rake +200 -0
- data/tasks/git.rake +40 -0
- data/tasks/post_load.rake +34 -0
- data/tasks/rdoc.task +8 -0
- data/tasks/rubyforge.rake +55 -0
- data/tasks/setup.rb +286 -0
- data/tasks/spec.rake +54 -0
- data/tasks/svn.rake +47 -0
- data/tasks/test.rake +40 -0
- data/version.txt +1 -0
- metadata +102 -0
data/History.txt
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2010 Eric Monti
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
'Software'), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
17
|
+
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
18
|
+
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
19
|
+
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
20
|
+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
= ffi-ssdeep
|
2
|
+
|
3
|
+
Ruby FFI bindings for the ssdeep 'libfuzzy' library api. This API lets you do
|
4
|
+
fuzzy hash comparisons between files and arbitrary string buffers. Fuzzy
|
5
|
+
hashes are also known as context triggered piecewise hashes (CTPH).
|
6
|
+
|
7
|
+
See the ssdeep homepage for more information:
|
8
|
+
http://ssdeep.sourceforge.net
|
9
|
+
|
10
|
+
== Requirements
|
11
|
+
|
12
|
+
* ffi >= 0.6.0
|
13
|
+
* ssdeep's libfuzzy - http://ssdeep.sourceforge.net
|
14
|
+
The version of ssdeep known to work with ffi-ssdeep is 2.5.
|
15
|
+
|
16
|
+
== Installation
|
17
|
+
|
18
|
+
First ensure you have installed the ssdeep package and that libfuzzy is in your libpath.
|
19
|
+
Then, just run
|
20
|
+
|
21
|
+
(sudo)? gem install ffi-ssdeep
|
22
|
+
|
23
|
+
== License
|
24
|
+
|
25
|
+
See LICENSE.txt
|
26
|
+
|
27
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# Look in the tasks/setup.rb file for the various options that can be
|
2
|
+
# configured in this Rakefile. The .rake files in the tasks directory
|
3
|
+
# are where the options are used.
|
4
|
+
|
5
|
+
load 'tasks/setup.rb'
|
6
|
+
|
7
|
+
ensure_in_path 'lib'
|
8
|
+
|
9
|
+
task :default => 'spec:run'
|
10
|
+
|
11
|
+
PROJ.name = 'ffi-ssdeep'
|
12
|
+
PROJ.authors = 'Eric Monti'
|
13
|
+
PROJ.email = 'emonti@trustwave.com'
|
14
|
+
PROJ.description = 'FFI bindings for the ssdeep library "libfuzzy" for fuzzy hash comparisons'
|
15
|
+
PROJ.url = nil
|
16
|
+
PROJ.version = File.open("version.txt","r"){|f| f.readline.chomp}
|
17
|
+
PROJ.readme_file = 'README.rdoc'
|
18
|
+
|
19
|
+
PROJ.spec.opts << '--color'
|
20
|
+
PROJ.rdoc.opts << '--line-numbers'
|
21
|
+
PROJ.notes.tags << "X"+"XX" # muhah! so we don't note our-self
|
22
|
+
|
23
|
+
# exclude rcov.rb and external libs from rcov report
|
24
|
+
PROJ.rcov.opts += [
|
25
|
+
"--exclude", "rcov",
|
26
|
+
"--exclude", "ffi",
|
27
|
+
]
|
28
|
+
|
29
|
+
depend_on 'ffi', '>= 0.6.0'
|
30
|
+
|
31
|
+
# EOF
|
Binary file
|
data/lib/ssdeep.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'ffi'
|
2
|
+
|
3
|
+
require 'ssdeep/fuzzy_hash'
|
4
|
+
|
5
|
+
module Ssdeep
|
6
|
+
extend FFI::Library
|
7
|
+
|
8
|
+
ffi_lib 'fuzzy'
|
9
|
+
|
10
|
+
typedef :pointer, :fuzzy_hash
|
11
|
+
|
12
|
+
# Compute the fuzzy hash of a buffer.
|
13
|
+
#
|
14
|
+
# Computes the fuzzy hash of the first buf_len bytes of the buffer. It is the caller's
|
15
|
+
# responsibility to append the filename, if any, to result after computation.
|
16
|
+
#
|
17
|
+
# Parameters:
|
18
|
+
# buf The data to be fuzzy hashed
|
19
|
+
# buf_len The length of the data being hashed
|
20
|
+
# result Where the fuzzy hash of buf is stored. This variable
|
21
|
+
# must be allocated to hold at least FUZZY_MAX_RESULT bytes.
|
22
|
+
#
|
23
|
+
# Returns:
|
24
|
+
# Returns zero on success, non-zero on error.
|
25
|
+
attach_function :fuzzy_hash_buf, [:pointer, :uint32, :fuzzy_hash], :int
|
26
|
+
|
27
|
+
# Compute the fuzzy hash of a file.
|
28
|
+
#
|
29
|
+
# Opens, reads, and hashes the contents of the file 'filename' The
|
30
|
+
# result must be allocated to hold FUZZY_MAX_RESULT characters. It
|
31
|
+
# is the caller's responsibility to append the filename to the
|
32
|
+
# result after computation.
|
33
|
+
#
|
34
|
+
# Parameters:
|
35
|
+
# filename The file to be hashed
|
36
|
+
# result Where the fuzzy hash of the file is stored. This variable
|
37
|
+
# must be allocated to hold at least FUZZY_MAX_RESULT bytes.
|
38
|
+
#
|
39
|
+
# Returns:
|
40
|
+
# Returns zero on success, non-zero on error.
|
41
|
+
attach_function :fuzzy_hash_filename, [:string, :fuzzy_hash], :int
|
42
|
+
|
43
|
+
# Computes the match score between two fuzzy hash signatures.
|
44
|
+
#
|
45
|
+
# Returns:
|
46
|
+
# Returns a value from zero to 100 indicating the match score of the
|
47
|
+
# two signatures. A match score of zero indicates the sigantures did
|
48
|
+
# not match. When an error occurs, such as if one of the inputs is
|
49
|
+
# NULL, returns -1.
|
50
|
+
attach_function :fuzzy_compare, [:fuzzy_hash, :fuzzy_hash], :int
|
51
|
+
|
52
|
+
|
53
|
+
def self.hash_string(str)
|
54
|
+
FuzzyHash.from_string(str)
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.hash_file(fname)
|
58
|
+
FuzzyHash.from_file(fname)
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.compare_hashes(h1, h2)
|
62
|
+
h1.compare(h2)
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.compare_strings(s1, s2)
|
66
|
+
compare_hashes(hash_string(s1), hash_string(s2))
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.compare_files(f1, f2)
|
70
|
+
compare_hashes(hash_file(f1), hash_file(f2))
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
|
2
|
+
require 'ffi'
|
3
|
+
|
4
|
+
module Ssdeep
|
5
|
+
SPAMSUM_LENGTH = 64
|
6
|
+
FUZZY_MAX_RESULT = (SPAMSUM_LENGTH + (SPAMSUM_LENGTH/2 + 20))
|
7
|
+
|
8
|
+
class FuzzyHash < FFI::MemoryPointer
|
9
|
+
def initialize()
|
10
|
+
super(FUZZY_MAX_RESULT)
|
11
|
+
end
|
12
|
+
|
13
|
+
# returns the computed fuzzy hash as a string
|
14
|
+
def to_s
|
15
|
+
self.read_string()
|
16
|
+
end
|
17
|
+
|
18
|
+
# implements a _dump method for Marshal
|
19
|
+
def _dump(depth)
|
20
|
+
self.to_s
|
21
|
+
end
|
22
|
+
|
23
|
+
# @return [Integer]
|
24
|
+
# Returns a value from zero to 100 indicating the match score of the
|
25
|
+
# two signatures. A match score of zero indicates the sigantures did
|
26
|
+
# not match.
|
27
|
+
#
|
28
|
+
# @return [
|
29
|
+
# When an error occurs, such as if one of the inputs is NULL.
|
30
|
+
def compare(other)
|
31
|
+
unless other.is_a?(FuzzyHash)
|
32
|
+
raise(TypeError, "a FuzzyHash can only be compared to another FuzzyHash")
|
33
|
+
end
|
34
|
+
if self.to_s == other.to_s
|
35
|
+
return 100
|
36
|
+
elsif (ret=Ssdeep.fuzzy_compare(self, other)) > -1
|
37
|
+
return ret
|
38
|
+
else
|
39
|
+
raise(StandardError, "unknown fuzzy hash comparison error")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# implements a _load method for Marshal
|
44
|
+
def self._load(raw)
|
45
|
+
from_hash(raw)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Restores a fuzzy hash that has already been generated and supplied
|
49
|
+
# as a string as a instance of a FuzzyHash object.
|
50
|
+
def self.from_hash(raw)
|
51
|
+
fh = new()
|
52
|
+
fh.write_string(
|
53
|
+
if raw.size < FUZZY_MAX_RESULT
|
54
|
+
raw + "\x00"
|
55
|
+
else
|
56
|
+
raw[0,FUZZY_MAX_RESULT]
|
57
|
+
end )
|
58
|
+
return fh
|
59
|
+
end
|
60
|
+
|
61
|
+
# Creates a new fuzzy hash from a string buffer.
|
62
|
+
def self.from_string(buf)
|
63
|
+
fh = new()
|
64
|
+
p = FFI::MemoryPointer.new(buf.size)
|
65
|
+
p.write_string(buf)
|
66
|
+
ret = Ssdeep.fuzzy_hash_buf(p, buf.size, fh)
|
67
|
+
if ret == 0
|
68
|
+
return fh
|
69
|
+
else
|
70
|
+
fh.free
|
71
|
+
raise(StandardError, "An error occurred hashing a string")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
# Creates a new fuzzy hash from the contents of 'filename'
|
76
|
+
def self.from_file(filename)
|
77
|
+
fh = new()
|
78
|
+
ret = Ssdeep.fuzzy_hash_filename(filename, fh)
|
79
|
+
if ret == 0
|
80
|
+
return fh
|
81
|
+
else
|
82
|
+
fh.free
|
83
|
+
raise(StandardError, "An error occurred hashing file: #{filename.inspect}")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Ssdeep::FuzzyHash do
|
4
|
+
context "basic features" do
|
5
|
+
before(:all) do
|
6
|
+
@fh1 = Ssdeep::FuzzyHash.from_file(sample_file("ssdeep.gemspec"))
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should support a from_string class method to hash from a string buffer" do
|
10
|
+
Ssdeep::FuzzyHash.from_file(sample_file("ssdeep.gemspec")).should be_kind_of(Ssdeep::FuzzyHash)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should support a from_string class method to hash from a string buffer" do
|
14
|
+
Ssdeep::FuzzyHash.from_string("helu").should be_kind_of(Ssdeep::FuzzyHash)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "should support a compare method to compare one FuzzyHash to another" do
|
18
|
+
fh2 = Ssdeep.hash_file(sample_file("ffi-ssdeep.gemspec"))
|
19
|
+
sh = Ssdeep.hash_string("helu")
|
20
|
+
@fh1.compare(@fh1).should == 100
|
21
|
+
@fh1.compare(fh2).should > 90
|
22
|
+
@fh1.compare(sh).should == 0
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should raise an exception when comparing null hash data" do
|
26
|
+
hh = Ssdeep::FuzzyHash.new()
|
27
|
+
lambda{ @fh1.compare(hh)}.should raise_error(StandardError)
|
28
|
+
lambda{ hh.compare(@fh1)}.should raise_error(StandardError)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
context "marshal serialization" do
|
33
|
+
before(:all) do
|
34
|
+
@fh = Ssdeep::FuzzyHash.from_file(sample_file("ssdeep.gemspec"))
|
35
|
+
@ser = Marshal.dump(@fh)
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should be serializable with Marshal.dump()" do
|
39
|
+
@ser.should be_kind_of(String)
|
40
|
+
@ser.should_not be_empty
|
41
|
+
@ser.index(@fh.to_s).should_not be_nil
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should be unserializable with Marshal.load()" do
|
45
|
+
fh_restore = Marshal.load(@ser)
|
46
|
+
fh_restore.should be_kind_of(Ssdeep::FuzzyHash)
|
47
|
+
fh_restore.compare(@fh).should == 100
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
data/spec/spec.opts
ADDED
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
SPEC_DIR = File.expand_path(File.dirname(__FILE__))
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(SPEC_DIR)
|
4
|
+
$LOAD_PATH.unshift(File.join(SPEC_DIR, '..', 'lib'))
|
5
|
+
|
6
|
+
require 'ssdeep'
|
7
|
+
require 'spec'
|
8
|
+
require 'spec/autorun'
|
9
|
+
|
10
|
+
SAMPLE_DIR = File.join(SPEC_DIR, 'samples')
|
11
|
+
|
12
|
+
def sample_message(filename)
|
13
|
+
dat = File.read(sample_file(filename))
|
14
|
+
dat.force_encoding('ASCII-8BIT') if RUBY_VERSION >= "1.9"
|
15
|
+
dat
|
16
|
+
end
|
17
|
+
|
18
|
+
def sample_file(filename)
|
19
|
+
File.join(SAMPLE_DIR, filename)
|
20
|
+
end
|
21
|
+
|
22
|
+
Spec::Runner.configure do |config|
|
23
|
+
end
|
data/spec/ssdeep_spec.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Ssdeep do
|
4
|
+
|
5
|
+
it "should have a compare_strings method that compares two strings" do
|
6
|
+
str1 = File.read(__FILE__)
|
7
|
+
str2 = str1.dup; str2[10,0]="insert some other junk"
|
8
|
+
Ssdeep.compare_strings(str1, str1).should == 100
|
9
|
+
Ssdeep.compare_strings(str2, str1).should < 100
|
10
|
+
Ssdeep.compare_strings(str2, str1).should >= 80
|
11
|
+
Ssdeep.compare_strings(str1, "something else").should == 0
|
12
|
+
Ssdeep.compare_strings(str2, "something else").should == 0
|
13
|
+
end
|
14
|
+
|
15
|
+
it "should have a compare_files method that compares two files" do
|
16
|
+
f1 = __FILE__
|
17
|
+
f2 = File.join(File.dirname(__FILE__), "spec_helper.rb")
|
18
|
+
Ssdeep.compare_files(f1, f1).should == 100
|
19
|
+
Ssdeep.compare_files(f1, f2 ).should < 20
|
20
|
+
Ssdeep.compare_files(f1, f2 ).should >= 0
|
21
|
+
end
|
22
|
+
|
23
|
+
it "should raise an error when compare_files is given a bad filename" do
|
24
|
+
f1 = __FILE__
|
25
|
+
f2 = sample_file("bogus_file.dat")
|
26
|
+
lambda{ Ssdeep.compare_files(f1,f2) }.should raise_error(StandardError)
|
27
|
+
lambda{ Ssdeep.compare_files(f2,f1) }.should raise_error(StandardError)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should have a compare_hashes method that compares two hashes" do
|
31
|
+
str1 = File.read(__FILE__)
|
32
|
+
str2 = str1.dup; str2[10,0]="insert some other junk"
|
33
|
+
h1 = Ssdeep::FuzzyHash.from_string(str1)
|
34
|
+
h2 = Ssdeep::FuzzyHash.from_string(str2)
|
35
|
+
h3 = Ssdeep::FuzzyHash.from_string("something_else")
|
36
|
+
Ssdeep.compare_hashes(h1, h1).should == 100
|
37
|
+
Ssdeep.compare_hashes(h2, h2).should == 100
|
38
|
+
Ssdeep.compare_hashes(h1, h2).should < 100
|
39
|
+
Ssdeep.compare_hashes(h1, h2).should >= 80
|
40
|
+
Ssdeep.compare_hashes(h1, h3).should == 0
|
41
|
+
Ssdeep.compare_hashes(h2, h3).should == 0
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should raise an exception when compare_hashes is given a bad argument" do
|
45
|
+
h1 = Ssdeep::FuzzyHash.from_string("some_data")
|
46
|
+
lambda{ Ssdeep.compare_hashes(h1, "not a hash") }.should raise_error(TypeError)
|
47
|
+
lambda{ Ssdeep.compare_hashes("not a hash", h1) }.should raise_error(StandardError)
|
48
|
+
end
|
49
|
+
|
50
|
+
it "should have a hash_string method that generates a fuzzy hash from a string" do
|
51
|
+
fh = Ssdeep.hash_string("this is a test string")
|
52
|
+
fh.should be_kind_of Ssdeep::FuzzyHash
|
53
|
+
fh.to_s.should == "3:YKEpFZ2:YfrI"
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should have a hash_file method that generates a fuzzy hash from a filename" do
|
57
|
+
fh = Ssdeep.hash_file(sample_file("ssdeep.gemspec"))
|
58
|
+
fh.should be_kind_of Ssdeep::FuzzyHash
|
59
|
+
fh.to_s.should == "24:ZkR5abenafgr2ph4glPlXeh/Tzj1wfgrUERNvnNyx/7/i/vA:fbenDr2ph9S/TzjvrUQvnoh/i/I"
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should raise an error when hash_file is given a bad filename" do
|
63
|
+
f1 = sample_file("bogus_file.dat")
|
64
|
+
lambda{ Ssdeep.hash_file(f1) }.should raise_error(StandardError)
|
65
|
+
end
|
66
|
+
|
67
|
+
|
68
|
+
end
|
69
|
+
|
data/tasks/ann.rake
ADDED
@@ -0,0 +1,80 @@
|
|
1
|
+
|
2
|
+
begin
|
3
|
+
require 'bones/smtp_tls'
|
4
|
+
rescue LoadError
|
5
|
+
require 'net/smtp'
|
6
|
+
end
|
7
|
+
require 'time'
|
8
|
+
|
9
|
+
namespace :ann do
|
10
|
+
|
11
|
+
# A prerequisites task that all other tasks depend upon
|
12
|
+
task :prereqs
|
13
|
+
|
14
|
+
file PROJ.ann.file do
|
15
|
+
ann = PROJ.ann
|
16
|
+
puts "Generating #{ann.file}"
|
17
|
+
File.open(ann.file,'w') do |fd|
|
18
|
+
fd.puts("#{PROJ.name} version #{PROJ.version}")
|
19
|
+
fd.puts(" by #{Array(PROJ.authors).first}") if PROJ.authors
|
20
|
+
fd.puts(" #{PROJ.url}") if PROJ.url.valid?
|
21
|
+
fd.puts(" (the \"#{PROJ.release_name}\" release)") if PROJ.release_name
|
22
|
+
fd.puts
|
23
|
+
fd.puts("== DESCRIPTION")
|
24
|
+
fd.puts
|
25
|
+
fd.puts(PROJ.description)
|
26
|
+
fd.puts
|
27
|
+
fd.puts(PROJ.changes.sub(%r/^.*$/, '== CHANGES'))
|
28
|
+
fd.puts
|
29
|
+
ann.paragraphs.each do |p|
|
30
|
+
fd.puts "== #{p.upcase}"
|
31
|
+
fd.puts
|
32
|
+
fd.puts paragraphs_of(PROJ.readme_file, p).join("\n\n")
|
33
|
+
fd.puts
|
34
|
+
end
|
35
|
+
fd.puts ann.text if ann.text
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
desc "Create an announcement file"
|
40
|
+
task :announcement => ['ann:prereqs', PROJ.ann.file]
|
41
|
+
|
42
|
+
desc "Send an email announcement"
|
43
|
+
task :email => ['ann:prereqs', PROJ.ann.file] do
|
44
|
+
ann = PROJ.ann
|
45
|
+
from = ann.email[:from] || Array(PROJ.authors).first || PROJ.email
|
46
|
+
to = Array(ann.email[:to])
|
47
|
+
|
48
|
+
### build a mail header for RFC 822
|
49
|
+
rfc822msg = "From: #{from}\n"
|
50
|
+
rfc822msg << "To: #{to.join(',')}\n"
|
51
|
+
rfc822msg << "Subject: [ANN] #{PROJ.name} #{PROJ.version}"
|
52
|
+
rfc822msg << " (#{PROJ.release_name})" if PROJ.release_name
|
53
|
+
rfc822msg << "\n"
|
54
|
+
rfc822msg << "Date: #{Time.new.rfc822}\n"
|
55
|
+
rfc822msg << "Message-Id: "
|
56
|
+
rfc822msg << "<#{"%.8f" % Time.now.to_f}@#{ann.email[:domain]}>\n\n"
|
57
|
+
rfc822msg << File.read(ann.file)
|
58
|
+
|
59
|
+
params = [:server, :port, :domain, :acct, :passwd, :authtype].map do |key|
|
60
|
+
ann.email[key]
|
61
|
+
end
|
62
|
+
|
63
|
+
params[3] = PROJ.email if params[3].nil?
|
64
|
+
|
65
|
+
if params[4].nil?
|
66
|
+
STDOUT.write "Please enter your e-mail password (#{params[3]}): "
|
67
|
+
params[4] = STDIN.gets.chomp
|
68
|
+
end
|
69
|
+
|
70
|
+
### send email
|
71
|
+
Net::SMTP.start(*params) {|smtp| smtp.sendmail(rfc822msg, from, to)}
|
72
|
+
end
|
73
|
+
end # namespace :ann
|
74
|
+
|
75
|
+
desc 'Alias to ann:announcement'
|
76
|
+
task :ann => 'ann:announcement'
|
77
|
+
|
78
|
+
CLOBBER << PROJ.ann.file
|
79
|
+
|
80
|
+
# EOF
|