dmarc_report 1.0

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/bin/dmarc_imap.rb ADDED
@@ -0,0 +1,416 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # = dmarc_imap.rb
4
+ #
5
+ # Author:: Dirk Meyer
6
+ # Copyright:: Copyright (c) 2020 - 2024 Dirk Meyer
7
+ # License:: Distributes under the same terms as Ruby
8
+ # SPDX-FileCopyrightText: 2020-2024 Dirk Meyer
9
+ # SPDX-License-Identifier: Ruby
10
+ #
11
+ # Parse IMAP accounts for DMARC reports and save them to files
12
+ # Input: IMAP folder
13
+ # Output: defined in the rulesets
14
+ #
15
+
16
+ # inspired by
17
+ # http://wonko.com/post/ruby_script_to_sync_email_from_any_imap_server_to_gmail
18
+
19
+ # dependecies:
20
+ # gem install --user-install new_rfc_2047
21
+
22
+ require 'openssl'
23
+ require 'net/imap'
24
+ require 'yaml'
25
+ require 'date'
26
+ # require 'rfc_2047'
27
+
28
+ # Maximum number of messages to select at once.
29
+ UID_BLOCK_SIZE = 1024
30
+ # Lockfile filename
31
+ LOCKFILENAME = '/tmp/dmarc-imap.lck'.freeze
32
+ # Lockfile timeout
33
+ LOCKTIMEOUT = 60 * 60 # 60 min
34
+
35
+ # https://stackoverflow.com/questions/7488875/how-to-decode-an-rfc-2047-encoded-email-header-in-ruby
36
+ # see also: https://github.com/ConradIrwin/rfc2047-ruby/
37
+ # see also: https://github.com/tonytonyjan/rfc_2047/
38
+ module Rfc2047
39
+ TOKEN = /[\041\043-\047\052\053\055\060-\071\101-\132\134\136\137\141-\176]+/.freeze
40
+ ENCODED_TEXT = /[\041-\076\100-\176]+/.freeze
41
+ ENCODED_WORD = /=\?(?<charset>#{TOKEN})\?(?<encoding>[QqBb])\?(?<encoded_text>#{ENCODED_TEXT})\?=/i.freeze
42
+
43
+ class << self
44
+ # encode text
45
+ def encode( input )
46
+ "=?#{input.encoding}?B?#{[ input ].pack( 'm0' )}?="
47
+ end
48
+
49
+ # decode text
50
+ def decode( input )
51
+ match_data = ENCODED_WORD.match( input )
52
+ # raise ArgumentError if match_data.nil?
53
+ # not encoded
54
+ return input if match_data.nil?
55
+
56
+ charset, encoding, encoded_text = match_data.captures
57
+ decoded =
58
+ case encoding
59
+ when 'Q', 'q' then encoded_text.unpack1( 'M' )
60
+ when 'B', 'b' then encoded_text.unpack1( 'm' )
61
+ end
62
+ decoded.force_encoding( charset )
63
+ begin
64
+ return decoded.encode( 'utf-8' )
65
+ rescue Encoding::InvalidByteSequenceError
66
+ decoded.force_encoding( 'BINARY' )
67
+ warn "InvalidByteSequenceError in #{decoded}"
68
+ end
69
+ decoded
70
+ end
71
+ end
72
+ end
73
+
74
+ # fetch a block of mails
75
+ def uid_fetch_block( server, uids, *args, &block )
76
+ pos = 0
77
+ while pos < uids.size
78
+ server.uid_fetch( uids[ pos, UID_BLOCK_SIZE ], *args ).each( &block )
79
+ pos += UID_BLOCK_SIZE
80
+ end
81
+ end
82
+
83
+ # report bad headers
84
+ def debug_headers( data )
85
+ puts 'Error in Header:'
86
+ puts data.inspect
87
+ end
88
+
89
+ # decode a header to UTF-8
90
+ def save_decode( key, text )
91
+ return nil if text.nil?
92
+
93
+ begin
94
+ text = Rfc2047.decode( text )
95
+ rescue Encoding::CompatibilityError => e
96
+ warn "Encoding::CompatibilityError #{e}"
97
+ warn "in #{key}: #{text}"
98
+ rescue StandardError => e
99
+ warn e
100
+ warn "in #{key}: #{text}"
101
+ end
102
+ text
103
+ end
104
+
105
+ # decode all headers to UTF-8
106
+ def decode_header( headers )
107
+ headers.each_pair do |key, list|
108
+ list.map! { |text| save_decode( key, text ) }
109
+ end
110
+ headers
111
+ end
112
+
113
+ # fetch all headers of a mail
114
+ def fetch_header( data )
115
+ headers = {}
116
+ last = nil
117
+ index = nil
118
+ return headers if data.attr[ 'RFC822.HEADER' ].nil?
119
+
120
+ data.attr[ 'RFC822.HEADER' ].split( "\r\n" ).each do |line|
121
+ # p line
122
+ case line
123
+ when /^[ \t]/ # continuation line
124
+ if last.nil? || index.nil?
125
+ debug_headers( line )
126
+ return headers
127
+ end
128
+ headers[ last ][ index ] << line
129
+ next
130
+ end
131
+
132
+ # new header line
133
+ key, val = line.split( ':', 2 )
134
+ if val.nil?
135
+ debug_headers( line )
136
+ return headers
137
+ end
138
+ val.strip!
139
+ if headers.key?( key )
140
+ headers[ key ].push( val )
141
+ else
142
+ headers[ key ] = [ val ]
143
+ end
144
+ last = key
145
+ index = headers[ key ].size - 1
146
+ end
147
+ decode_header( headers )
148
+ end
149
+
150
+ # fetch body of a mail
151
+ def fetch_body( imap, data )
152
+ key = data.attr[ 'UID' ]
153
+ body = data.attr[ 'RFC822.HEADER' ]
154
+ # pp key
155
+ body << imap.uid_fetch( key, 'BODY[TEXT]' )[ 0 ].attr[ 'BODY[TEXT]' ]
156
+ # pp body
157
+ body
158
+ end
159
+
160
+ # find a header matching the given ruleset
161
+ def find_header( rule, headers, key, header )
162
+ unless rule.key?( key )
163
+ return nil # no rule
164
+ end
165
+ unless headers.key?( header )
166
+ # puts "no header '#{header}'"
167
+ return false # no header
168
+ end
169
+
170
+ headers[ header ].each do |line|
171
+ if line.include?( rule[ key ] )
172
+ puts "# found: #{rule[ key ]}"
173
+ return true
174
+ end
175
+ end
176
+ # puts "no match '#{header}'"
177
+ false # no match
178
+ end
179
+
180
+ # select and create a IMAP folder
181
+ def create_folder( dest, dest_folder )
182
+ puts "Selecting folder '#{dest_folder}'..."
183
+ dest.select( dest_folder )
184
+ rescue Net::IMAP::NoResponseError => e
185
+ begin
186
+ warn 'Folder not found; creating...'
187
+ dest.create( dest_folder )
188
+ dest.subscribe( dest_folder )
189
+ dest.select( dest_folder )
190
+ rescue Net::IMAP::NoResponseError => ee
191
+ warn ee.inspect
192
+ @cancel = true
193
+ nil
194
+ rescue StandardError => ee
195
+ warn "Error: could not create folder: #{e}: #{ee}"
196
+ exit 1
197
+ end
198
+ end
199
+
200
+ # strip zone from date line
201
+ def clean_date( date )
202
+ date.sub( / \([A-Z]+\)$/, '' )
203
+ end
204
+
205
+ # save mail as file and enumerate in case of duplicates
206
+ def run_save( dir, file, body )
207
+ dest = "#{dir}/#{file}"
208
+ unless File.exist?( dest )
209
+ File.write( dest, body )
210
+ return true
211
+ end
212
+ puts "Warning: file exist: #{dest}"
213
+ old = File.read( dest )
214
+ return true if old == body
215
+
216
+ 2.upto( 5 ) do |i|
217
+ dest = "#{dir}/#{file}.#{i}"
218
+ unless File.exist?( dest )
219
+ File.write( dest, body )
220
+ return true
221
+ end
222
+ end
223
+
224
+ false
225
+ end
226
+
227
+ # use IMAP path syntax
228
+ def map_target( target )
229
+ return target unless @translate_slash
230
+
231
+ target.gsub( '/', '.' )
232
+ end
233
+
234
+ # move mail into target folder
235
+ def move_target( imap, imap_uid, target )
236
+ target = map_target( target )
237
+ create_folder( imap, target ) if @create_folder
238
+ puts "# move: #{target}"
239
+ imap.uid_copy( imap_uid, target )
240
+ imap.uid_store( imap_uid, '+FLAGS', [ :Deleted ] )
241
+ end
242
+
243
+ # strip filename from unwanted characters
244
+ def clean_filename( filename )
245
+ filename.gsub( /[^[:print:]]/, '_' ).sub( '/', '_' )
246
+ end
247
+
248
+ # execute ruleset on given mail
249
+ def run_action( imap, rule, imap_uid, headers, data )
250
+ if rule.key?( 'move' )
251
+ move_target( imap, imap_uid, rule[ 'move' ] )
252
+ return
253
+ end
254
+ return unless rule.key?( 'save' )
255
+
256
+ dir = rule[ 'save' ][ 'dir' ]
257
+ unless headers.key?( 'Date' )
258
+ warn 'Error: no Date-Header'
259
+ return
260
+ end
261
+ unless headers.key?( 'Subject' )
262
+ warn 'Error: no Subject-Header'
263
+ return
264
+ end
265
+ puts "# save: #{dir}"
266
+ pp headers[ 'Date' ].first
267
+ pp clean_date( headers[ 'Date' ].first )
268
+ file = Date.rfc2822( clean_date( headers[ 'Date' ].first ) ).to_s
269
+ file << ' '
270
+ file << headers[ 'Subject' ].first
271
+ body = fetch_body( imap, data )
272
+ return unless run_save( dir, clean_filename( file ), body )
273
+
274
+ move_target( imap, imap_uid, rule[ 'save' ][ 'move' ] )
275
+ end
276
+
277
+ # check all rulesets of given mail
278
+ def run_rules( imap, rules, headers, data )
279
+ moved = false
280
+ rules.each do |rule|
281
+ [ 'from', 'to', 'subject' ].each do |key|
282
+ header = key.capitalize
283
+ next unless find_header( rule, headers, key, header )
284
+
285
+ run_action( imap, rule, data.attr[ 'UID' ], headers, data )
286
+ moved = true
287
+ break
288
+ end
289
+ break if moved
290
+
291
+ next unless rule.key?( 'match' )
292
+ next unless headers.key?( rule[ 'header' ] )
293
+
294
+ headers[ rule[ 'header' ] ].each do |line|
295
+ next unless line.include?( rule[ 'match' ] )
296
+
297
+ puts "# found: #{rule[ 'match' ]}"
298
+ run_action( imap, rule, data.attr[ 'UID' ], headers, data )
299
+ moved = true
300
+ break
301
+ end
302
+ break if moved
303
+ end
304
+ end
305
+
306
+ # check input folder for mails
307
+ def run_folder( imap, folder, filename )
308
+ rules = YAML.load_file( filename )
309
+ # puts rules.inspect
310
+
311
+ begin
312
+ imap.select( folder )
313
+ rescue Net::IMAP::NoResponseError => e
314
+ warn e.inspect
315
+ warn "Folder '#{folder}' not found; abort..."
316
+ return
317
+ end
318
+
319
+ suids = imap.uid_search( [ 'ALL' ] )
320
+ puts "folder = '#{folder}' messages = #{suids.length}"
321
+ return unless suids.length.positive?
322
+
323
+ uid_fetch_block( imap, suids, [ 'RFC822.HEADER' ] ) do |data|
324
+ break if @cancel
325
+
326
+ headers = fetch_header( data )
327
+ if @debug
328
+ pp headers
329
+ puts headers[ 'Subject' ].first if headers.key?( 'Subject' )
330
+ puts
331
+ end
332
+ run_rules( imap, rules, headers, data )
333
+ end
334
+ end
335
+
336
+ # connect to IMAP server and parse mails
337
+ def run_sort( config )
338
+ imap = Net::IMAP.new( config[ 'host' ], { port: config[ 'port' ], ssl: { ca_file: config[ 'ca_file' ] } } )
339
+ imap.login( config[ 'login' ], config[ 'password' ] )
340
+
341
+ run_folder( imap, config[ 'folder' ], config[ 'rules' ] )
342
+
343
+ imap.expunge
344
+ imap.logout
345
+ imap.disconnect
346
+ end
347
+
348
+ # set options from configfile
349
+ def parse_options( config )
350
+ @translate_slash = config[ 'translate_slash' ]
351
+ @translate_slash = false if @translate_slash.nil?
352
+ @create_folder = config[ 'create_folder' ]
353
+ @create_folder = false if @create_folder.nil?
354
+ end
355
+
356
+ # parse arguments from commandline
357
+ def parse_arguments
358
+ @pattern = nil
359
+ @check_force = false
360
+ @debug = false
361
+ ARGV.each do |arg|
362
+ case arg
363
+ when 'force'
364
+ @check_force = true
365
+ when 'debug'
366
+ @debug = true
367
+ else
368
+ @pattern = argv
369
+ end
370
+ end
371
+ end
372
+
373
+ parse_arguments
374
+
375
+ # use a lockfile to limit parallel jobs
376
+ if File.exist?( LOCKFILENAME )
377
+ puts "locked: #{LOCKFILENAME}"
378
+ unless @check_force
379
+ mtime = File.stat( LOCKFILENAME ).mtime
380
+ exit 0 if mtime.to_i > Time.now.to_i - LOCKTIMEOUT
381
+ end
382
+
383
+ warn "unlocking: #{LOCKFILENAME}"
384
+ File.unlink( LOCKFILENAME )
385
+ end
386
+
387
+ File.write( LOCKFILENAME, Time.now.to_s )
388
+
389
+ # run on each given IMAP account
390
+ @cancel = false
391
+ dir = 'config'
392
+ Dir.foreach( dir ) do |file|
393
+ case file
394
+ when '.', '..'
395
+ next # skip unix entries
396
+ when /[.]yml$/
397
+ puts file
398
+ next if !@pattern.nil? && !/^#{@pattern}/.match( file )
399
+
400
+ config = YAML.load_file( "#{dir}/#{file}" )
401
+ parse_options( config )
402
+
403
+ # puts config.inspect
404
+ if config.key?( 'disable' )
405
+ puts 'disabled'
406
+ next
407
+ end
408
+
409
+ run_sort( config )
410
+ end
411
+ end
412
+
413
+ File.unlink( LOCKFILENAME )
414
+
415
+ exit 0
416
+ # eof
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # = dmarc_report.rb
4
+ #
5
+ # Author:: Dirk Meyer
6
+ # Copyright:: Copyright (c) 2020 - 2023 Dirk Meyer
7
+ # License:: Distributes under the same terms as Ruby
8
+ # SPDX-FileCopyrightText: 2020-2023 Dirk Meyer
9
+ # SPDX-License-Identifier: Ruby
10
+ #
11
+ # Parse directory with DMARC reports and extract the attachments
12
+ # Input: directory with extracted XML attachments
13
+ # Output: summary reports as CSV file
14
+ #
15
+
16
+ require 'nokogiri'
17
+ require 'csv'
18
+
19
+ $: << 'lib'
20
+
21
+ require 'xmltohash'
22
+
23
+ # directory with extracted XML attachments
24
+ XML_DIR = 'DMARC/xml'.freeze
25
+ # summary reports as CSV file
26
+ OUTPUT_FILE = 'dmarc-report.csv'.freeze
27
+
28
+ # convert tiemstamp to short ISO date
29
+ def date_text( secs )
30
+ Time.at( secs.to_i ).strftime( '%Y-%m-%d' )
31
+ end
32
+
33
+ # generate csv header
34
+ def titles
35
+ [ :ziel_server, :datum, :source_ip, :header_from, :envelope_to, :anzahl,
36
+ :dkim, :dkim_ergebnis, :spf, :spf_ergebnis, :local_policy ].map( &:to_s )
37
+ end
38
+
39
+ # parse auth_results entry
40
+ def auth_results_entry( entry, header_from )
41
+ if entry.key?( :domain )
42
+ entry[ :domain ]&.downcase!
43
+ return entry[ :result ] if entry[ :domain ] == header_from
44
+
45
+ return "#{entry[ :result ]}:#{entry[ :domain ]}"
46
+ end
47
+ return "#{entry[ :result ]}:#{entry[ :scope ]}" if entry.key?( :scope )
48
+
49
+ entry[ :result ]
50
+ end
51
+
52
+ # search auth_results
53
+ def auth_results_field( record, field, header_from )
54
+ return nil unless record[ :auth_results ].respond_to?( :key? )
55
+ return nil unless record[ :auth_results ].key?( field )
56
+
57
+ return auth_results_entry( record[ :auth_results ][ field ], header_from ) \
58
+ if record[ :auth_results ][ field ].respond_to?( :key? )
59
+
60
+ list = []
61
+ record[ :auth_results ][ field ].each do |entry|
62
+ list.push( auth_results_entry( entry, header_from ) )
63
+ end
64
+ list.join( ' ' )
65
+ end
66
+
67
+ # search for arc results
68
+ def local_results( hash )
69
+ return nil if hash.nil?
70
+ return nil unless hash.key?( :type )
71
+ return hash[ :type ] if hash[ :type ] == 'mailing_list'
72
+ return "type=#{hash[ :type ]}" if hash[ :type ] != 'local_policy'
73
+ return hash[ :type ] unless hash.key?( :comment )
74
+ return hash[ :type ] if hash[ :comment ].nil?
75
+
76
+ hash[ :comment ]
77
+ end
78
+
79
+ # add cvs row
80
+ def add_row( hash, record )
81
+ # pp record
82
+ pp record
83
+ header_from = record[ :identifiers ][ :header_from ]
84
+ header_from&.downcase!
85
+ @log.push( [
86
+ hash[ :feedback ][ :report_metadata ][ :email ].split( '@' ).last,
87
+ date_text( hash[ :feedback ][ :report_metadata ][ :date_range ][ :begin ] ),
88
+ record[ :row ][ :source_ip ],
89
+ header_from,
90
+ record[ :identifiers ][ :envelope_to ],
91
+ record[ :row ][ :count ].to_i,
92
+ record[ :row ][ :policy_evaluated ][ :dkim ],
93
+ auth_results_field( record, :dkim, header_from ),
94
+ record[ :row ][ :policy_evaluated ][ :spf ],
95
+ auth_results_field( record, :spf, header_from ),
96
+ local_results( record[ :row ][ :policy_evaluated ][ :reason ] )
97
+ ] )
98
+ pp @log.last
99
+ end
100
+
101
+ # parse xml file
102
+ def run_xml( fullname )
103
+ p fullname
104
+ raw = File.read( fullname )
105
+ return if raw.empty?
106
+ return if raw == 'unused'
107
+
108
+ begin
109
+ h = Hash.from_xml( raw )
110
+ rescue NoMethodError
111
+ warn "Bad XML in #{fullname}"
112
+ pp 'raw'
113
+ return
114
+ end
115
+ pp h
116
+ # AOL can send empty XML files
117
+ return if h.nil?
118
+
119
+ records = h[ :feedback ][ :record ]
120
+ # pp records
121
+ if records.respond_to?( :key? )
122
+ add_row( h, records )
123
+ return
124
+ end
125
+ records.each do |record|
126
+ add_row( h, record )
127
+ end
128
+ end
129
+
130
+ # parse xml directory
131
+ def parse_xml_dir
132
+ Dir.entries( XML_DIR ).each do |file|
133
+ fullname = "#{XML_DIR}/#{file}"
134
+ case file
135
+ when /^[.]/
136
+ next
137
+ when /[.]xml$/, /[.]xml_[0-9]*$/
138
+ run_xml( fullname )
139
+ next
140
+ end
141
+ p fullname
142
+ puts 'Aborted extension'
143
+ exit 1
144
+ end
145
+ end
146
+
147
+ @log = []
148
+ parse_xml_dir
149
+ # pp @log
150
+
151
+ # write csv
152
+ @log.sort_by! { |r| r[ 1 ] }
153
+ CSV.open( OUTPUT_FILE, 'wb+', col_sep: ';' ) do |csv|
154
+ csv << titles
155
+ @log.each do |row|
156
+ csv << row
157
+ end
158
+ end
159
+
160
+ exit 0
161
+ # eof
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # = dmarc_ripmime.rb
4
+ #
5
+ # Author:: Dirk Meyer
6
+ # Copyright:: Copyright (c) 2020 - 2023 Dirk Meyer
7
+ # License:: Distributes under the same terms as Ruby
8
+ # SPDX-FileCopyrightText: 2020-2023 Dirk Meyer
9
+ # SPDX-License-Identifier: Ruby
10
+ #
11
+ # Parse directory with DMARC reports and extract the attachments
12
+ # Input: file with mail
13
+ # Output: decompressed attachments
14
+ #
15
+
16
+ # dependecies:
17
+ # apt-get install ripmime
18
+
19
+ # directory with new reports
20
+ INPUT_DIR = 'DMARC/in'.freeze
21
+ # directory with processed reports
22
+ SEEN_DIR = 'DMARC/seen'.freeze
23
+ # directory with new attachments
24
+ RIPMIME_DIR = 'DMARC/ripmime'.freeze
25
+ # directory with processed attachments
26
+ ZIP_DIR = 'DMARC/zips'.freeze
27
+ # directory with extracted attachments
28
+ XML_DIR = 'DMARC/xml'.freeze
29
+ # working directory for unzip
30
+ TMP_DIR = 'DMARC/tmp'.freeze
31
+
32
+ # start ripmime
33
+ def run_ripmime( src, dest )
34
+ line = "ripmime -i '#{src}' -d '#{RIPMIME_DIR}'"
35
+ rc = system( line )
36
+ if rc
37
+ # puts "mv '#{src}' '#{dest}'"
38
+ File.rename( src, dest )
39
+ return
40
+ end
41
+ warn 'ABORTED: ripmime'
42
+ exit 1
43
+ end
44
+
45
+ # search for new reports
46
+ def parse_input_dir
47
+ Dir.entries( INPUT_DIR ).each do |file|
48
+ next if file =~ /^[.]/
49
+
50
+ fullname = "#{INPUT_DIR}/#{file}"
51
+ target = "#{SEEN_DIR}/#{file}"
52
+ p fullname
53
+ run_ripmime( fullname, target )
54
+ end
55
+ end
56
+
57
+ # search for decompressed XML file
58
+ def parse_temp_dir
59
+ list = []
60
+ Dir.entries( TMP_DIR ).each do |file|
61
+ next if file =~ /^[.]/
62
+
63
+ list.push( file )
64
+ end
65
+ if list.empty?
66
+ puts 'no xml file found'
67
+ pp list
68
+ exit 1
69
+ end
70
+ if list.size != 1
71
+ puts 'to many xml files:'
72
+ list.each do |file|
73
+ puts "rm #{file}"
74
+ File.unlink( "#{TMP_DIR}/#{file}" )
75
+ end
76
+ exit 1
77
+ end
78
+ list.first
79
+ end
80
+
81
+ # decompress zip attachment
82
+ def unzip( fullname, target )
83
+ return if File.exist?( target )
84
+
85
+ line = "unzip -j -n -d '#{TMP_DIR}' '#{fullname}'"
86
+ p line
87
+ rc = system( line )
88
+ if rc
89
+ src = "#{TMP_DIR}/#{parse_temp_dir}"
90
+ # puts "mv '#{src}' '#{target}'"
91
+ File.rename( src.to_s, target )
92
+ return
93
+ end
94
+ warn 'ABORTED: unzip'
95
+ exit 1
96
+ end
97
+
98
+ # decompress gzip attachment
99
+ def ungzip( fullname, target )
100
+ return if File.exist?( target )
101
+
102
+ line = "gunzip -c '#{fullname}' > '#{target}'"
103
+ p line
104
+ rc = system( line )
105
+ return if rc
106
+
107
+ p rc
108
+ warn 'ABORTED: gunzip'
109
+ exit 1
110
+ end
111
+
112
+ # search for new attachments
113
+ def parse_ripmime_dir
114
+ Dir.entries( RIPMIME_DIR ).each do |file|
115
+ fullname = "#{RIPMIME_DIR}/#{file}"
116
+ saved = "#{ZIP_DIR}/#{file}"
117
+ case file
118
+ when /^[.]/
119
+ next
120
+ when /[.]zip$/
121
+ # target = "#{XML_DIR}/#{file}".sub( /(_[0-9])*[.]zip$/, '.xml' )
122
+ target = "#{XML_DIR}/#{file}".sub( /[.]zip$/, '.xml' )
123
+ unzip( fullname, target )
124
+ File.rename( fullname, saved )
125
+ next
126
+ when /[.]xml(_[0-9])*[.]gz$/
127
+ # target = "#{XML_DIR}/#{file}".sub( /(_[0-9])*[.]gz$/, '' )
128
+ target = "#{XML_DIR}/#{file}".sub( /[.]gz$/, '' )
129
+ ungzip( fullname, target )
130
+ File.rename( fullname, saved )
131
+ next
132
+ when /^textfile/
133
+ File.unlink( fullname )
134
+ next
135
+ end
136
+ puts "IGNORED extension: #{fullname}"
137
+ end
138
+ end
139
+
140
+ parse_input_dir
141
+ parse_ripmime_dir
142
+
143
+ exit 0
144
+ # eof