notes-structured-text-json-messages 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ lib/**/*.rb
2
+ bin/*
3
+ -
4
+ features/**/*.feature
5
+ LICENSE.txt
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/LICENSE.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2011 Trampoline Systems Ltd
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
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,29 @@
1
+ = notes-structured-text-json-messages
2
+
3
+ A command-line tool for transforming Lotus Notes Structured Text exports into JSON message structures for SONAR import
4
+
5
+ notes_structured_text_json_messages <output_dir> <input_file> [<input_file>*]
6
+
7
+ = Dependencies
8
+ * actionmailer ~> 2.3.11
9
+ * RubyGems
10
+
11
+ = Install
12
+
13
+ gem install notes-structured-text-json-messages
14
+
15
+ == Contributing to notes-structured-text-json-messages
16
+
17
+ * Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet
18
+ * Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it
19
+ * Fork the project
20
+ * Start a feature/bugfix branch
21
+ * Commit and push until you are happy with your contribution
22
+ * Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
23
+ * Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
24
+
25
+ == Copyright
26
+
27
+ Copyright (c) 2011 Trampoline Systems Ltd. See LICENSE.txt for
28
+ further details.
29
+
data/Rakefile ADDED
@@ -0,0 +1,47 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ require 'jeweler'
5
+ Jeweler::Tasks.new do |gem|
6
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
7
+ gem.name = "notes-structured-text-json-messages"
8
+ gem.homepage = "http://github.com/trampoline/notes-structured-text-json-messages"
9
+ gem.license = "MIT"
10
+ gem.summary = %Q{produces json message descriptions from lotus notes structured text exports}
11
+ gem.description = %Q{parses lotus notes structured text exports, producing a json message file for each each message}
12
+ gem.email = "craig@trampolinesystems.com"
13
+ gem.authors = ["craig mcmillan"]
14
+ # Include your dependencies below. Runtime dependencies are required when using your gem,
15
+ # and development dependencies are only needed for development (ie running rake tasks, tests, etc)
16
+ # gem.add_runtime_dependency 'jabber4r', '> 0.1'
17
+ # gem.add_development_dependency 'rspec', '> 1.2.3'
18
+ gem.add_runtime_dependency "actionmailer", "~> 2.3.11"
19
+ gem.add_development_dependency "rspec", "~> 1.3.0"
20
+ gem.add_development_dependency "jeweler", "~> 1.5.2"
21
+ gem.add_development_dependency "rcov", ">= 0"
22
+ end
23
+ Jeweler::RubygemsDotOrgTasks.new
24
+
25
+ require 'spec/rake/spectask'
26
+ Spec::Rake::SpecTask.new(:spec) do |spec|
27
+ spec.libs << 'lib' << 'spec'
28
+ spec.spec_files = FileList['spec/**/*_spec.rb']
29
+ end
30
+
31
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
32
+ spec.libs << 'lib' << 'spec'
33
+ spec.pattern = 'spec/**/*_spec.rb'
34
+ spec.rcov = true
35
+ end
36
+
37
+ task :default => :spec
38
+
39
+ require 'rake/rdoctask'
40
+ Rake::RDocTask.new do |rdoc|
41
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
42
+
43
+ rdoc.rdoc_dir = 'rdoc'
44
+ rdoc.title = "notes-structured-text-json-messages #{version}"
45
+ rdoc.rdoc_files.include('README*')
46
+ rdoc.rdoc_files.include('lib/**/*.rb')
47
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.1
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env ruby
2
+ $: << File.expand_path("../../lib", __FILE__)
3
+ require 'rubygems'
4
+ require 'action_mailer'
5
+ require 'notes_structured_text_json_messages'
6
+ require 'optparse'
7
+ require 'logger'
8
+
9
+ options={:verbose=>true}
10
+
11
+ USAGE = "Usage: #{File.basename(__FILE__)} <output_dir> <input_files> [<input_file>]* "
12
+
13
+ OptionParser.new do |opts|
14
+ opts.banner = "Usage: #{File.basename(__FILE__)} <output_dir> <input_files> [<input_file>]* "
15
+
16
+ opts.on("-v", "--[no-]verbose", "Run verbosely (default: true)") do |v|
17
+ options[:verbose] = v
18
+ end
19
+ end.parse!
20
+
21
+ NotesStructuredTextJsonMessages.logger = Logger.new($stderr)
22
+ if options[:verbose]
23
+ NotesStructuredTextJsonMessages.logger.level=Logger::INFO
24
+ else
25
+ NotesStructuredTextJsonMessages.logger.level=Logger::WARN
26
+ end
27
+
28
+ begin
29
+ raise "insufficient arguments" if ARGV.length<2
30
+
31
+ output_dir = ARGV[0]
32
+ input_files = ARGV[1..-1]
33
+
34
+ NotesStructuredTextJsonMessages.json_messages(output_dir, input_files, options)
35
+ NotesStructuredTextJsonMessages.log{|logger| logger.info("complete")}
36
+ rescue Exception=>e
37
+ NotesStructuredTextJsonMessages.log{|logger| logger.error(e)}
38
+ NotesStructuredTextJsonMessages.log{|logger| logger.info(USAGE)}
39
+ exit(1)
40
+ end
41
+ exit(0)
@@ -0,0 +1,228 @@
1
+ require 'md5'
2
+ require 'tmail'
3
+
4
+ module NotesStructuredTextJsonMessages
5
+ class << self
6
+ attr_accessor :logger
7
+ attr_accessor :stats
8
+ end
9
+
10
+ module_function
11
+
12
+ def log
13
+ yield logger if logger
14
+ end
15
+
16
+ def reset_stats
17
+ self.stats={}
18
+ end
19
+
20
+ def increment_stats(key)
21
+ self.stats[key] = (self.stats[key]||0) + 1
22
+ end
23
+
24
+ def json_messages(output_dir, input_files, options={})
25
+ reset_stats
26
+ [*input_files].each do |input_file|
27
+ File.open(input_file, "r") do |input|
28
+ json_messages_from_stream(output_dir, input, options)
29
+ end
30
+ end
31
+ stats.each do |k,v|
32
+ log{|logger| logger.info("#{k}: #{v}")}
33
+ end
34
+ end
35
+
36
+ def json_messages_from_stream(output_dir, input, options={})
37
+ block = nil
38
+ process_block(output_dir, block, options) while block=read_block(input)
39
+ end
40
+
41
+ def read_block(input)
42
+ return nil if input.eof?
43
+ block = []
44
+ begin
45
+ l = input.readline.chomp
46
+ block << l if l.length>0
47
+ end while !input.eof? && l != ""
48
+ block
49
+ end
50
+
51
+ def is_message_block?(block)
52
+ !!header_value(block, MESSAGE_ID)
53
+ end
54
+
55
+ def is_distinguished_name?(addr)
56
+ !!(addr =~ /CN=/)
57
+ end
58
+
59
+ def header_value(block, header)
60
+ patt = /^#{Regexp.quote(header)}: /i
61
+ h = block.find{|l| l =~ patt}
62
+ h.gsub(patt, '').strip if h
63
+ end
64
+
65
+ def header_values(block, header, split_on=",")
66
+ h = header_value(block, header)
67
+ if h
68
+ if split_on.is_a?(Symbol)
69
+ self.send(split_on, h)
70
+ else
71
+ h.split(split_on)
72
+ end.map(&:strip)
73
+ end
74
+ end
75
+
76
+ def split_rfc822_addresses(header)
77
+ addresses = []
78
+ quoted_pair = false
79
+ quoted_string = false
80
+ buf = ""
81
+ header.each_char do |c|
82
+ if quoted_pair
83
+ buf << c
84
+ quoted_pair = false
85
+ elsif quoted_string && c=='\\'
86
+ buf << c
87
+ quoted_pair = true
88
+ elsif !quoted_string && c==','
89
+ addresses << buf
90
+ buf = ""
91
+ elsif !quoted_string && c=='"'
92
+ buf << c
93
+ quoted_string = true
94
+ elsif quoted_string && c=='"'
95
+ buf << c
96
+ quoted_string = false
97
+ else
98
+ buf << c
99
+ end
100
+ end
101
+ addresses << buf if buf.length>0
102
+ addresses
103
+ end
104
+
105
+ def strip_angles(value)
106
+ value.gsub(/<([^>]*)>/, '\1')
107
+ end
108
+
109
+ def process_block(output_dir, block, options={})
110
+ if is_message_block?(block)
111
+ json_message = extract_json_message(block, options)
112
+ output_json_message(output_dir, json_message)
113
+ increment_stats(:message)
114
+ else
115
+ increment_stats(:non_message)
116
+ end
117
+ rescue Exception=>e
118
+ increment_stats(:failed_message)
119
+ log do |logger|
120
+ logger.error(e)
121
+ logger.error(block.join("\n"))
122
+ end
123
+ end
124
+
125
+ MESSAGE_ID = "$MessageID"
126
+ POSTED_DATE = "PostedDate"
127
+ IN_REPLY_TO = "in_reply_to"
128
+ REFERENCES = "references"
129
+ FROM = "From"
130
+ TO = "SendTo"
131
+ CC = "CopyTo"
132
+ BCC = "BlindCopyTo"
133
+ INET_FROM = "InetFrom"
134
+ INET_TO = "InetSendTo"
135
+ INET_CC = "InetCopyTo"
136
+ INET_BCC = "InetBlindCopyTo"
137
+
138
+ def process_address(addr)
139
+ if is_distinguished_name?(addr)
140
+ name = addr[/CN=([^\/]*)/, 1]
141
+ { :name=>name,
142
+ :notes_dn=>addr}
143
+ else
144
+ ta = TMail::Address.parse(addr)
145
+ if ta.is_a?(TMail::Address)
146
+ { :name=>ta.name,
147
+ :email_address=>ta.address.downcase}
148
+ else
149
+ log{|logger| logger.warn("addr does not parse to a TMail::Address: #{addr}")}
150
+ end
151
+ end
152
+ end
153
+
154
+ def process_address_pair(inet_addr, notes_addr)
155
+ if inet_addr == "."
156
+ process_address(notes_addr)
157
+ else
158
+ process_address(inet_addr)
159
+ end
160
+ end
161
+
162
+ def process_addresses(block, inet_field, notes_field)
163
+ inet_h = header_values(block, inet_field, :split_rfc822_addresses)
164
+ notes_h = header_values(block, notes_field, :split_rfc822_addresses)
165
+
166
+ if inet_h && notes_h
167
+ if inet_h.length == notes_h.length
168
+ inet_h.zip(notes_h).map do |inet_addr, notes_addr|
169
+ process_address_pair(inet_addr, notes_addr)
170
+ end
171
+ else
172
+ raise "#{inet_field}: does not match #{notes_field}:"
173
+ end
174
+ elsif inet_h
175
+ inet_h.map{|addr| process_address(addr)}
176
+ elsif notes_h
177
+ notes_h.map{|addr| process_address(addr)}
178
+ else
179
+ nil
180
+ end
181
+ end
182
+
183
+ NOTES_US_DATE_FORMAT = "%m/%d/%Y %I:%M:%S %p"
184
+
185
+ def parse_date(date, options={})
186
+ DateTime.strptime(date, NOTES_US_DATE_FORMAT)
187
+ end
188
+
189
+ def extract_json_message(block, options={})
190
+ message_id_h = header_value(block, MESSAGE_ID)
191
+ raise "no #{MESSAGE_ID}" if !message_id_h
192
+ message_id = strip_angles(message_id_h)
193
+
194
+ posted_date_h = header_value(block, POSTED_DATE)
195
+ raise "no #{POSTED_DATE}" if !posted_date_h
196
+ posted_date = parse_date(posted_date_h, options)
197
+
198
+ in_reply_to_h = header_value(block, IN_REPLY_TO)
199
+ in_reply_to = strip_angles(in_reply_to_h) if in_reply_to_h
200
+
201
+ references_h = header_values(block, REFERENCES, " ")
202
+ references = references_h.map{|r| strip_angles(r)} if references_h
203
+
204
+ froms = process_addresses(block, INET_FROM, FROM)
205
+ raise "no From:, or more than one From:" if !froms || froms.size>1
206
+ from = froms[0]
207
+ to = process_addresses(block, INET_TO, TO)
208
+ cc = process_addresses(block, INET_CC, CC)
209
+ bcc = process_addresses(block, INET_BCC, BCC)
210
+
211
+ raise "no recipients" if (to||[]).size + (cc||[]).size + (bcc||[]).size == 0
212
+
213
+ { :message_type=>"email",
214
+ :message_id=>message_id,
215
+ :sent_at=>posted_date,
216
+ :in_reply_to=>in_reply_to,
217
+ :references=>references,
218
+ :from=>from,
219
+ :to=>to,
220
+ :cc=>cc,
221
+ :bcc=>bcc}
222
+ end
223
+
224
+ def output_json_message(output_dir, json_message)
225
+ fname = File.join(output_dir, MD5.hexdigest(json_message[:message_id]))
226
+ File.open(fname, "w"){|out| out << json_message.to_json}
227
+ end
228
+ end
@@ -0,0 +1,507 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ describe NotesStructuredTextJsonMessages do
4
+ describe "json_messages" do
5
+ it "should open each file and call json_messages_from_stream with it" do
6
+ output_dir = Object.new
7
+ input_files = [Object.new, Object.new]
8
+ options = Object.new
9
+
10
+ input0 = Object.new
11
+ input1 = Object.new
12
+ mock(File).open(input_files[0], "r"){|f,opts,block| block.call(input0)}
13
+ mock(File).open(input_files[1], "r"){|f,opts,block| block.call(input1)}
14
+
15
+ mock(NotesStructuredTextJsonMessages).json_messages_from_stream(output_dir, input0, options)
16
+ mock(NotesStructuredTextJsonMessages).json_messages_from_stream(output_dir, input1, options)
17
+
18
+ NotesStructuredTextJsonMessages.json_messages(output_dir, input_files, options)
19
+ end
20
+ end
21
+
22
+ describe "json_messages_from_stream" do
23
+ it "should call process_block for each block retrieved from the stream" do
24
+ output_dir = Object.new
25
+ input = Object.new
26
+ blocks = [nil, ["foo", "bar"], ["baz", "boo"]]
27
+
28
+ mock(NotesStructuredTextJsonMessages).read_block(input).times(3){blocks.pop}
29
+
30
+ mock(NotesStructuredTextJsonMessages).process_block(output_dir, ["baz", "boo"], anything)
31
+ mock(NotesStructuredTextJsonMessages).process_block(output_dir, ["foo", "bar"], anything)
32
+
33
+ NotesStructuredTextJsonMessages.json_messages_from_stream(output_dir, input)
34
+ end
35
+ end
36
+
37
+ describe "readblock" do
38
+ it "should return lines read from a stream until the first empty line" do
39
+ input = <<-EOF
40
+ foo
41
+ bar
42
+
43
+ baz
44
+ boo
45
+ EOF
46
+ io = StringIO.new(input)
47
+ NotesStructuredTextJsonMessages.read_block(io).should == ["foo", "bar"]
48
+ NotesStructuredTextJsonMessages.read_block(io).should == ["baz", "boo"]
49
+ end
50
+
51
+ it "should return nil if the input stream is at EOF" do
52
+ io = StringIO.new("foo\nbar")
53
+ NotesStructuredTextJsonMessages.read_block(io).should == ["foo", "bar"]
54
+ NotesStructuredTextJsonMessages.read_block(io).should == nil
55
+ end
56
+ end
57
+
58
+ describe "is_distinguished_name?" do
59
+ it "should return true if the address contains a CN=... string" do
60
+ NotesStructuredTextJsonMessages.is_distinguished_name?("CN=foo bar/OU=before/O=after").should == true
61
+ end
62
+
63
+ it "should return false if there is no CN=... " do
64
+ NotesStructuredTextJsonMessages.is_distinguished_name?("foo@bar.com").should == false
65
+ end
66
+ end
67
+
68
+ describe "is_message_block?" do
69
+ it "should return true if the block contains a line which start with '$MessageID: ' " do
70
+ NotesStructuredTextJsonMessages.is_message_block?( ["foo", "$MessageID: bar", "baz"] ).should == true
71
+ end
72
+
73
+ it "should return false if there are no lines starting with '$MessageID: ' in the block" do
74
+ NotesStructuredTextJsonMessages.is_message_block?( ["foo", "bar", "baz"] ).should == false
75
+ end
76
+
77
+ it "should not be case-sensitive" do
78
+ NotesStructuredTextJsonMessages.is_message_block?( ["foo", "$MESSAGEID: bar", "baz"] ).should == true
79
+ NotesStructuredTextJsonMessages.is_message_block?( ["foo", "$messageID: bar", "baz"] ).should == true
80
+ NotesStructuredTextJsonMessages.is_message_block?( ["foo", "$messageid: bar", "baz"] ).should == true
81
+ end
82
+ end
83
+
84
+ describe "header_value" do
85
+ it "should extract the first occurence of a header from the block" do
86
+ NotesStructuredTextJsonMessages.header_value(["Foo: foo", "Bar: bar", "Baz: baz"], "Foo").should == "foo"
87
+ end
88
+
89
+ it "should strip whitespace" do
90
+ NotesStructuredTextJsonMessages.header_value(["Foo: foo ", "Bar: bar", "Baz: baz"], "Foo").should == "foo"
91
+ end
92
+ it "should return nil if there are no occurences of the heaer in the block" do
93
+ NotesStructuredTextJsonMessages.header_value(["Boo: foo", "Bar: bar", "Baz: baz"], "Foo").should == nil
94
+ end
95
+
96
+ it "should not be case-sensitive" do
97
+ NotesStructuredTextJsonMessages.header_value(["fOO: foo", "Bar: bar", "Baz: baz"], "Foo").should == "foo"
98
+ end
99
+ end
100
+
101
+ describe "header_values" do
102
+ it "should extract an array of values for the header if present" do
103
+ NotesStructuredTextJsonMessages.header_values(["Foo: foo", "Bar: bar", "Baz: baz"], "Foo").should == ["foo"]
104
+ NotesStructuredTextJsonMessages.header_values(["Foo: a,b,c", "Bar: bar", "Baz: baz"], "Foo").should == ["a", "b", "c"]
105
+ end
106
+
107
+ it "should return nil if there are no occurences of the header" do
108
+ NotesStructuredTextJsonMessages.header_values(["Boo: a,b,c", "Bar: bar", "Baz: baz"], "Foo").should == nil
109
+ end
110
+
111
+ it "should strip whitespace" do
112
+ NotesStructuredTextJsonMessages.header_values(["Foo: a ,\tb , c ", "Bar: bar", "Baz: baz"], "Foo").should == ["a", "b", "c"]
113
+ end
114
+
115
+ it "should strip on a given character" do
116
+ NotesStructuredTextJsonMessages.header_values(["Foo: a b c", "Bar: c d e", "Baz: f g h"], "Bar", " ").should == ["c", "d", "e"]
117
+ end
118
+
119
+ it "should strip with a given method" do
120
+ NotesStructuredTextJsonMessages.header_values(['SendTo: "mcfoo, foo" <foo.mcfoo@foo.com>,"bar \\"barry\\" mcbar" <bar.mcbar@bar.com>'],
121
+ "SendTo",
122
+ :split_rfc822_addresses).should ==
123
+ ['"mcfoo, foo" <foo.mcfoo@foo.com>',
124
+ '"bar \\"barry\\" mcbar" <bar.mcbar@bar.com>']
125
+ end
126
+ end
127
+
128
+ describe "split_rfc822_addresses" do
129
+ it "should do nothing to a single address" do
130
+ NotesStructuredTextJsonMessages.split_rfc822_addresses('"foo mcfoo" <foo.mcfoo@foo.com>').should ==
131
+ ['"foo mcfoo" <foo.mcfoo@foo.com>']
132
+ end
133
+
134
+ it "should split a simple case" do
135
+ NotesStructuredTextJsonMessages.split_rfc822_addresses('"foo mcfoo" <foo.mcfoo@foo.com>,"bar mcbar" <bar.mcbar@bar.com>').should ==
136
+ ['"foo mcfoo" <foo.mcfoo@foo.com>','"bar mcbar" <bar.mcbar@bar.com>']
137
+ end
138
+
139
+ it "should split if there is a comma inside a quoted string" do
140
+ NotesStructuredTextJsonMessages.split_rfc822_addresses('"mcfoo, foo" <foo.mcfoo@foo.com>,"bar mcbar" <bar.mcbar@bar.com>').should ==
141
+ ['"mcfoo, foo" <foo.mcfoo@foo.com>','"bar mcbar" <bar.mcbar@bar.com>']
142
+ end
143
+
144
+ it "should split if there is a quoted double-qoute within a string" do
145
+ NotesStructuredTextJsonMessages.split_rfc822_addresses('"foo \\"foo foo\\" mcfoo" <foo.mcfoo@foo.com>,"bar mcbar" <bar.mcbar@bar.com>').should ==
146
+ ['"foo \\"foo foo\\" mcfoo" <foo.mcfoo@foo.com>','"bar mcbar" <bar.mcbar@bar.com>']
147
+ end
148
+ end
149
+
150
+ describe "strip_angles" do
151
+ it "should remove angle-brackets if present" do
152
+ NotesStructuredTextJsonMessages.strip_angles("<foo>").should == "foo"
153
+ end
154
+
155
+ it "should do nothing if no angle brackets present" do
156
+ NotesStructuredTextJsonMessages.strip_angles("foo").should == "foo"
157
+ end
158
+
159
+ it "should do nothing if a single angle bracket present" do
160
+ NotesStructuredTextJsonMessages.strip_angles("<foo").should == "<foo"
161
+ NotesStructuredTextJsonMessages.strip_angles("foo>").should == "foo>"
162
+ end
163
+ end
164
+
165
+ describe "process_block" do
166
+ it "should output_json_message if is_message_block?" do
167
+ output_dir = Object.new
168
+ block = Object.new
169
+ json_message = Object.new
170
+
171
+ stub(NotesStructuredTextJsonMessages).is_message_block?(block){true}
172
+ mock(NotesStructuredTextJsonMessages).extract_json_message(block, anything){json_message}
173
+ mock(NotesStructuredTextJsonMessages).output_json_message(output_dir, json_message)
174
+
175
+ NotesStructuredTextJsonMessages.process_block(output_dir, block)
176
+ end
177
+
178
+ it "should ignore if !is_message_block?" do
179
+ output_dir = Object.new
180
+ block = Object.new
181
+
182
+ stub(NotesStructuredTextJsonMessages).is_message_block?(block){false}
183
+ dont_allow(NotesStructuredTextJsonMessages).extract_json_message.with_any_args
184
+ dont_allow(NotesStructuredTextJsonMessages).output_json_message.with_any_args
185
+
186
+ NotesStructuredTextJsonMessages.process_block(output_dir, block)
187
+ end
188
+
189
+ it "should catch and log exceptions during processing" do
190
+ output_dir = Object.new
191
+ block = ["foo", "bar"]
192
+ logger = Object.new
193
+
194
+ stub(NotesStructuredTextJsonMessages).logger{logger}
195
+ stub(NotesStructuredTextJsonMessages).is_message_block?(block){true}
196
+ stub(NotesStructuredTextJsonMessages).extract_json_message(block, anything){raise "boo"}
197
+
198
+ mock(logger).error(anything) do |err|
199
+ err.is_a?(Exception).should == true
200
+ err.message.should =~ /boo/
201
+ end
202
+ mock(logger).error(block.join("\n"))
203
+
204
+ lambda {
205
+ NotesStructuredTextJsonMessages.process_block(output_dir, block)
206
+ }.should_not raise_error
207
+ end
208
+ end
209
+
210
+ describe "process_address" do
211
+ it "should produce a notes_dn hash if is_distinguished_name?" do
212
+ NotesStructuredTextJsonMessages.process_address("CN=foo bar/OU=here/O=there").should ==
213
+ {:name=>"foo bar", :notes_dn=>"CN=foo bar/OU=here/O=there"}
214
+ end
215
+
216
+ it "should be case-preserving for distinguished names" do
217
+ NotesStructuredTextJsonMessages.process_address("CN=Foo Bar/OU=Here/O=There").should ==
218
+ {:name=>"Foo Bar", :notes_dn=>"CN=Foo Bar/OU=Here/O=There"}
219
+ end
220
+
221
+ it "should parse with TMail::Address if !is_distinguished_name?" do
222
+ NotesStructuredTextJsonMessages.process_address('"foo bar" <foo@bar.com>').should ==
223
+ {:name=>"foo bar", :email_address=>"foo@bar.com"}
224
+ end
225
+
226
+ it "should downcase internet email addresses" do
227
+ NotesStructuredTextJsonMessages.process_address('"Foo Bar" <Foo@Bar.com>').should ==
228
+ {:name=>"Foo Bar", :email_address=>"foo@bar.com"}
229
+ end
230
+
231
+ it "should log a warning if an internet email address does not parse to a TMail::Address" do
232
+ logger = Object.new
233
+ stub(NotesStructuredTextJsonMessages).logger{logger}
234
+
235
+ mock(logger).warn(/does not parse .* TMail::Address/)
236
+
237
+ NotesStructuredTextJsonMessages.process_address('Undisclosed recipients:;').should == nil
238
+ end
239
+ end
240
+
241
+ describe "process_address_pair" do
242
+ it "should process_address the notes_address if the inet_address is '.'" do
243
+ NotesStructuredTextJsonMessages.process_address_pair(".", "CN=foo bar/OU=here/O=there").should ==
244
+ {:name=>"foo bar", :notes_dn=>"CN=foo bar/OU=here/O=there"}
245
+ end
246
+
247
+ it "should process_address the inet_addr if the inet_address is not '.'" do
248
+ NotesStructuredTextJsonMessages.process_address_pair('"foo bar" <foo@bar.com>', "CN=foo bar/OU=here/O=there").should ==
249
+ {:name=>"foo bar", :email_address=>"foo@bar.com"}
250
+ end
251
+ end
252
+
253
+ describe "process_addresses" do
254
+ it "should raise an exception if the notes and inet headers do not match" do
255
+ inet_field = Object.new
256
+ notes_field = Object.new
257
+ block = Object.new
258
+
259
+ stub(NotesStructuredTextJsonMessages).header_values(block, inet_field, :split_rfc822_addresses){["foo", "bar"]}
260
+ stub(NotesStructuredTextJsonMessages).header_values(block, notes_field, :split_rfc822_addresses){["foo"]}
261
+
262
+ lambda {
263
+ NotesStructuredTextJsonMessages.process_addresses(block, inet_field, notes_field)
264
+ }.should raise_error(/does not match/)
265
+ end
266
+
267
+ it "should call process_address_pair if notes and inet headers are both present" do
268
+ inet_field = Object.new
269
+ notes_field = Object.new
270
+ block = Object.new
271
+
272
+ stub(NotesStructuredTextJsonMessages).header_values(block, inet_field, :split_rfc822_addresses){["foo.mcfoo@foo.com", "bar.mcbar@bar.com"]}
273
+ stub(NotesStructuredTextJsonMessages).header_values(block, notes_field, :split_rfc822_addresses){["CN=foo mcfoo/OU=main/O=foo", "CN=bar mcbar/OU=main/O=bar"]}
274
+
275
+ mock(NotesStructuredTextJsonMessages).process_address_pair("foo.mcfoo@foo.com", "CN=foo mcfoo/OU=main/O=foo")
276
+ mock(NotesStructuredTextJsonMessages).process_address_pair("bar.mcbar@bar.com", "CN=bar mcbar/OU=main/O=bar")
277
+
278
+ NotesStructuredTextJsonMessages.process_addresses(block, inet_field, notes_field)
279
+ end
280
+
281
+ it "should call process_address if an inet header is present" do
282
+ inet_field = Object.new
283
+ notes_field = Object.new
284
+ block = Object.new
285
+
286
+ stub(NotesStructuredTextJsonMessages).header_values(block, inet_field, :split_rfc822_addresses){["foo.mcfoo@foo.com", "bar.mcbar@bar.com"]}
287
+ stub(NotesStructuredTextJsonMessages).header_values(block, notes_field, :split_rfc822_addresses){nil}
288
+
289
+ mock(NotesStructuredTextJsonMessages).process_address("foo.mcfoo@foo.com")
290
+ mock(NotesStructuredTextJsonMessages).process_address("bar.mcbar@bar.com")
291
+
292
+ NotesStructuredTextJsonMessages.process_addresses(block, inet_field, notes_field)
293
+ end
294
+
295
+ it "should call process_address if a notes header is present" do
296
+ inet_field = Object.new
297
+ notes_field = Object.new
298
+ block = Object.new
299
+
300
+ stub(NotesStructuredTextJsonMessages).header_values(block, inet_field, :split_rfc822_addresses){nil}
301
+ stub(NotesStructuredTextJsonMessages).header_values(block, notes_field, :split_rfc822_addresses){["CN=foo mcfoo/OU=main/O=foo", "CN=bar mcbar/OU=main/O=bar"]}
302
+
303
+ mock(NotesStructuredTextJsonMessages).process_address("CN=foo mcfoo/OU=main/O=foo")
304
+ mock(NotesStructuredTextJsonMessages).process_address("CN=bar mcbar/OU=main/O=bar")
305
+
306
+ NotesStructuredTextJsonMessages.process_addresses(block, inet_field, notes_field)
307
+ end
308
+
309
+ end
310
+
311
+ describe "output_json_message" do
312
+ it "should write the json encoded message to a file named by the MD5 of the message_id" do
313
+ output_dir = "/a/b/c/d"
314
+ json_message = Object.new
315
+ json_struct = {:message_id=>"foo123@foo.com"}
316
+
317
+ stub(json_message).[](:message_id){"foo123@foo.com"}
318
+ stub(json_message).to_json{json_struct.to_json}
319
+
320
+ output_stream = Object.new
321
+ mock(output_stream).<<(json_struct.to_json){output_stream}
322
+
323
+ mock(File).open("/a/b/c/d/#{MD5.hexdigest("foo123@foo.com")}", "w") do |fn,m,block|
324
+ block.call(output_stream)
325
+ end
326
+
327
+ NotesStructuredTextJsonMessages.output_json_message(output_dir, json_message)
328
+ end
329
+ end
330
+
331
+ describe "parse_date" do
332
+ it "should parse a US morning date correctly" do
333
+ dt = NotesStructuredTextJsonMessages.parse_date("01/25/2011 05:21:37 AM")
334
+ dt.is_a?(DateTime).should == true
335
+ dt.mday.should == 25
336
+ dt.mon.should == 01
337
+ dt.year.should == 2011
338
+ dt.hour.should == 5
339
+ dt.min.should == 21
340
+ dt.sec.should == 37
341
+ dt.zone.should == "+00:00"
342
+ end
343
+
344
+ it "should parse a US evening date correctly" do
345
+ dt = NotesStructuredTextJsonMessages.parse_date("12/01/2011 05:21:37 PM")
346
+ dt.is_a?(DateTime).should == true
347
+ dt.mday.should == 1
348
+ dt.mon.should == 12
349
+ dt.year.should == 2011
350
+ dt.hour.should == 17
351
+ dt.min.should == 21
352
+ dt.sec.should == 37
353
+ dt.zone.should == "+00:00"
354
+ end
355
+ end
356
+
357
+ describe "extract_json_message" do
358
+ def notes_message(options={})
359
+ h = {
360
+ "$MessageID" => "<foo123@foo.com>",
361
+ "PostedDate" => "02/25/2011 08:06:10 PM",
362
+ "In_Reply_To" => "<bar456@bar.com>",
363
+ "References" => "<bar456@bar.com> <ear789@ear.com>",
364
+ "From" => "CN=foo mcfoo/OU=fooclub/O=foo",
365
+ "SendTo" => "CN=bar mcbar/OU=barclub/O=bar,CN=baz mcbaz/OU=bazclub/O=baz",
366
+ "CopyTo" => "CN=dar mcdar/OU=darclub/O=dar,CN=ear mcear/OU=earclub/O=ear",
367
+ "BlindCopyTo" => "CN=far mcfar/OU=farclub/O=far,CN=gar mcgar/OU=garclub/O=gar"}.merge(options)
368
+ h.map{|k,v| "#{k}: #{v}" if v}
369
+ end
370
+
371
+ it "should raise an exception if there is no message-id" do
372
+ block = notes_message("$MessageID"=>nil)
373
+ lambda {
374
+ NotesStructuredTextJsonMessages.extract_json_message(block)
375
+ }.should raise_error(/no \$MessageID/)
376
+ end
377
+
378
+ it "should raise an exception if there is no From: or InetFrom:" do
379
+ block = notes_message("From"=>nil)
380
+
381
+ lambda {
382
+ NotesStructuredTextJsonMessages.extract_json_message(block)
383
+ }.should raise_error(/no From/)
384
+ end
385
+
386
+ it "should raise an exception if there are no recipients" do
387
+ block = notes_message("SendTo"=>nil, "CopyTo"=>nil, "BlindCopyTo"=>nil)
388
+
389
+ lambda {
390
+ NotesStructuredTextJsonMessages.extract_json_message(block)
391
+ }.should raise_error(/no recipients/)
392
+ end
393
+
394
+ it "should raise an exception if there is no PostedDate" do
395
+ block = notes_message("PostedDate"=>nil)
396
+
397
+ lambda {
398
+ NotesStructuredTextJsonMessages.extract_json_message(block)
399
+ }.should raise_error(/no PostedDate/)
400
+ end
401
+
402
+ it "should remove angle-brackets from message_id, in_reply_to and references" do
403
+ block = notes_message
404
+ j = NotesStructuredTextJsonMessages.extract_json_message(block)
405
+ j[:message_id].should == "foo123@foo.com"
406
+ end
407
+
408
+ it "should parse a US date correctly" do
409
+ block = notes_message
410
+ j = NotesStructuredTextJsonMessages.extract_json_message(block)
411
+ d = j[:sent_at]
412
+ d.is_a?(DateTime).should == true
413
+ d.mday.should == 25
414
+ d.month.should == 2
415
+ d.year.should == 2011
416
+ d.hour.should == 20
417
+ d.min.should == 6
418
+ d.sec.should == 10
419
+ end
420
+
421
+ it "should parse the From / InetFrom fields" do
422
+ block = notes_message
423
+ j = NotesStructuredTextJsonMessages.extract_json_message(block)
424
+ j[:from].should == {:notes_dn=>"CN=foo mcfoo/OU=fooclub/O=foo", :name=>"foo mcfoo"}
425
+
426
+ block = notes_message("From"=>'"foo mcfoo" <foo.mcfoo@foo.com>')
427
+ j = NotesStructuredTextJsonMessages.extract_json_message(block)
428
+ j[:from].should == {:email_address=>"foo.mcfoo@foo.com", :name=>"foo mcfoo"}
429
+
430
+ block = notes_message("InetFrom"=>'"foo mcfoo" <foo.mcfoo@foo.com>', "From"=>"CN=foo mcfoo/OU=fooclub/O=foo")
431
+ j = NotesStructuredTextJsonMessages.extract_json_message(block)
432
+ j[:from].should == {:email_address=>"foo.mcfoo@foo.com", :name=>"foo mcfoo"}
433
+
434
+ block = notes_message("InetFrom"=>'.', "From"=>'"foo mcfoo" <foo.mcfoo@foo.com>')
435
+ j = NotesStructuredTextJsonMessages.extract_json_message(block)
436
+ j[:from].should == {:email_address=>"foo.mcfoo@foo.com", :name=>"foo mcfoo"}
437
+ end
438
+
439
+ it "should parse the SendTo / InetSendTo fields" do
440
+ block = notes_message
441
+ j = NotesStructuredTextJsonMessages.extract_json_message(block)
442
+ j[:to].should == [{:notes_dn=>"CN=bar mcbar/OU=barclub/O=bar", :name=>"bar mcbar"},
443
+ {:notes_dn=>"CN=baz mcbaz/OU=bazclub/O=baz", :name=>"baz mcbaz"}]
444
+
445
+ block = notes_message("SendTo"=>'"bar mcbar" <bar.mcbar@bar.com>,"baz mcbaz" <baz.mcbaz@baz.com>')
446
+ j = NotesStructuredTextJsonMessages.extract_json_message(block)
447
+ j[:to].should == [{:email_address=>"bar.mcbar@bar.com", :name=>"bar mcbar"},
448
+ {:email_address=>"baz.mcbaz@baz.com", :name=>"baz mcbaz"}]
449
+
450
+ block = notes_message("InetSendTo"=>'"bar mcbar" <bar.mcbar@bar.com>,"baz mcbaz" <baz.mcbaz@baz.com>')
451
+ j = NotesStructuredTextJsonMessages.extract_json_message(block)
452
+ j[:to].should == [{:email_address=>"bar.mcbar@bar.com", :name=>"bar mcbar"},
453
+ {:email_address=>"baz.mcbaz@baz.com", :name=>"baz mcbaz"}]
454
+
455
+ block = notes_message("InetSendTo"=>'.,"baz mcbaz" <baz.mcbaz@baz.com>', "SendTo"=>'"bar mcbar" <bar.mcbar@bar.com>,"CN=baz mcbaz/OU=bazclub/O=baz"')
456
+ j = NotesStructuredTextJsonMessages.extract_json_message(block)
457
+ j[:to].should == [{:email_address=>"bar.mcbar@bar.com", :name=>"bar mcbar"},
458
+ {:email_address=>"baz.mcbaz@baz.com", :name=>"baz mcbaz"}]
459
+ end
460
+
461
+ it "should parse the CopyTo / InetCopyTo fields" do
462
+ block = notes_message
463
+ j = NotesStructuredTextJsonMessages.extract_json_message(block)
464
+ j[:cc].should == [{:notes_dn=>"CN=dar mcdar/OU=darclub/O=dar", :name=>"dar mcdar"},
465
+ {:notes_dn=>"CN=ear mcear/OU=earclub/O=ear", :name=>"ear mcear"}]
466
+
467
+ block = notes_message("CopyTo" => '"dar mcdar" <dar.mcdar@dar.com>,"ear mcear" <ear.mcear@ear.com>')
468
+ j = NotesStructuredTextJsonMessages.extract_json_message(block)
469
+ j[:cc].should == [{:email_address=>"dar.mcdar@dar.com", :name=>"dar mcdar"},
470
+ {:email_address=>"ear.mcear@ear.com", :name=>"ear mcear"}]
471
+
472
+ block = notes_message("InetCopyTo" => '"dar mcdar" <dar.mcdar@dar.com>,"ear mcear" <ear.mcear@ear.com>')
473
+ j = NotesStructuredTextJsonMessages.extract_json_message(block)
474
+ j[:cc].should == [{:email_address=>"dar.mcdar@dar.com", :name=>"dar mcdar"},
475
+ {:email_address=>"ear.mcear@ear.com", :name=>"ear mcear"}]
476
+
477
+ block = notes_message("InetCopyTo" => '.,"ear mcear" <ear.mcear@ear.com>',
478
+ "CopyTo" => '"dar mcdar" <dar.mcdar@dar.com>,CN=ear mcear/OU=earclub/O=ear')
479
+ j = NotesStructuredTextJsonMessages.extract_json_message(block)
480
+ j[:cc].should == [{:email_address=>"dar.mcdar@dar.com", :name=>"dar mcdar"},
481
+ {:email_address=>"ear.mcear@ear.com", :name=>"ear mcear"}]
482
+ end
483
+
484
+ it "should parse the BlindCopyTo / InetBlindCopyTo fields" do
485
+ block = notes_message
486
+ j = NotesStructuredTextJsonMessages.extract_json_message(block)
487
+ j[:bcc].should == [{:notes_dn=>"CN=far mcfar/OU=farclub/O=far", :name=>"far mcfar"},
488
+ {:notes_dn=>"CN=gar mcgar/OU=garclub/O=gar", :name=>"gar mcgar"}]
489
+
490
+ block = notes_message("BlindCopyTo" => '"far mcfar" <far.mcfar@far.com>,"gar mcgar" <gar.mcgar@gar.com>')
491
+ j = NotesStructuredTextJsonMessages.extract_json_message(block)
492
+ j[:bcc].should == [{:email_address=>"far.mcfar@far.com", :name=>"far mcfar"},
493
+ {:email_address=>"gar.mcgar@gar.com", :name=>"gar mcgar"}]
494
+
495
+ block = notes_message("InetBlindCopyTo" => '"far mcfar" <far.mcfar@far.com>,"gar mcgar" <gar.mcgar@gar.com>')
496
+ j = NotesStructuredTextJsonMessages.extract_json_message(block)
497
+ j[:bcc].should == [{:email_address=>"far.mcfar@far.com", :name=>"far mcfar"},
498
+ {:email_address=>"gar.mcgar@gar.com", :name=>"gar mcgar"}]
499
+
500
+ block = notes_message("InetBlindCopyTo" => '.,"gar mcgar" <gar.mcgar@gar.com>',
501
+ "BlindCopyTo" => '"far mcfar" <far.mcfar@far.com>,CN=gar mcgar/OU=garclub/O=gar')
502
+ j = NotesStructuredTextJsonMessages.extract_json_message(block)
503
+ j[:bcc].should == [{:email_address=>"far.mcfar@far.com", :name=>"far mcfar"},
504
+ {:email_address=>"gar.mcgar@gar.com", :name=>"gar mcgar"}]
505
+ end
506
+ end
507
+ end
@@ -0,0 +1,12 @@
1
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
3
+ require 'rubygems'
4
+ require 'spec'
5
+ require 'spec/autorun'
6
+ require 'rr'
7
+ require 'action_mailer'
8
+ require 'notes_structured_text_json_messages'
9
+
10
+ Spec::Runner.configure do |config|
11
+ config.mock_with RR::Adapters::Rspec
12
+ end
metadata ADDED
@@ -0,0 +1,139 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: notes-structured-text-json-messages
3
+ version: !ruby/object:Gem::Version
4
+ hash: 25
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 1
10
+ version: 0.1.1
11
+ platform: ruby
12
+ authors:
13
+ - craig mcmillan
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2011-04-26 00:00:00 +01:00
19
+ default_executable: notes_structured_text_json_messages
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: actionmailer
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ hash: 21
30
+ segments:
31
+ - 2
32
+ - 3
33
+ - 11
34
+ version: 2.3.11
35
+ type: :runtime
36
+ version_requirements: *id001
37
+ - !ruby/object:Gem::Dependency
38
+ name: rspec
39
+ prerelease: false
40
+ requirement: &id002 !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ~>
44
+ - !ruby/object:Gem::Version
45
+ hash: 27
46
+ segments:
47
+ - 1
48
+ - 3
49
+ - 0
50
+ version: 1.3.0
51
+ type: :development
52
+ version_requirements: *id002
53
+ - !ruby/object:Gem::Dependency
54
+ name: jeweler
55
+ prerelease: false
56
+ requirement: &id003 !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ hash: 7
62
+ segments:
63
+ - 1
64
+ - 5
65
+ - 2
66
+ version: 1.5.2
67
+ type: :development
68
+ version_requirements: *id003
69
+ - !ruby/object:Gem::Dependency
70
+ name: rcov
71
+ prerelease: false
72
+ requirement: &id004 !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ hash: 3
78
+ segments:
79
+ - 0
80
+ version: "0"
81
+ type: :development
82
+ version_requirements: *id004
83
+ description: parses lotus notes structured text exports, producing a json message file for each each message
84
+ email: craig@trampolinesystems.com
85
+ executables:
86
+ - notes_structured_text_json_messages
87
+ extensions: []
88
+
89
+ extra_rdoc_files:
90
+ - LICENSE.txt
91
+ - README.rdoc
92
+ files:
93
+ - .document
94
+ - .rspec
95
+ - LICENSE.txt
96
+ - README.rdoc
97
+ - Rakefile
98
+ - VERSION
99
+ - bin/notes_structured_text_json_messages
100
+ - lib/notes_structured_text_json_messages.rb
101
+ - spec/notes_structured_text_json_messages_spec.rb
102
+ - spec/spec_helper.rb
103
+ has_rdoc: true
104
+ homepage: http://github.com/trampoline/notes-structured-text-json-messages
105
+ licenses:
106
+ - MIT
107
+ post_install_message:
108
+ rdoc_options: []
109
+
110
+ require_paths:
111
+ - lib
112
+ required_ruby_version: !ruby/object:Gem::Requirement
113
+ none: false
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ hash: 3
118
+ segments:
119
+ - 0
120
+ version: "0"
121
+ required_rubygems_version: !ruby/object:Gem::Requirement
122
+ none: false
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ hash: 3
127
+ segments:
128
+ - 0
129
+ version: "0"
130
+ requirements: []
131
+
132
+ rubyforge_project:
133
+ rubygems_version: 1.6.2
134
+ signing_key:
135
+ specification_version: 3
136
+ summary: produces json message descriptions from lotus notes structured text exports
137
+ test_files:
138
+ - spec/notes_structured_text_json_messages_spec.rb
139
+ - spec/spec_helper.rb