ffi-ssdeep 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|