1#!/usr/bin/env ruby
2#
3# cmew.rb: Creating Message-ID: DB with Sqlite3
4#
5
6begin
7  require 'rubygems'
8  gem 'sqlite3-ruby'
9rescue LoadError
10end
11require 'sqlite3'
12
13require 'time'
14require 'fileutils'
15require 'find'
16
17################################################################
18##
19## mail_header
20##
21##	Get header from mail message.  If multiple header field
22##	exists, last one will be used.
23##
24def mail_header(path)
25  @header = {}
26  value = nil
27  File.open(path) do |f|
28    while l = f.gets.chomp
29      next if /^From / =~ l
30      break if /^$/ =~ l
31      if /^\s+/ !~ l
32        (name, value) = l.split(/:\s*/, 2)
33        value = '' if value.nil? || value.empty?
34        @header[name.downcase] = value
35      else
36        value << $'
37      end
38    end
39  end
40  return @header
41end
42
43################################################################
44##
45## DB
46##
47
48def open_db(db_file, fullupdate)
49  db_new_file = db_file + '.new'
50
51  if FileTest.exist?(db_new_file)
52    STDERR.print "Error: another cmew running.\n"
53    exit 1
54  end
55
56  if fullupdate == false
57    FileUtils.copy_file(db_file, db_new_file) if FileTest.exist?(db_file)
58  end
59
60  db = SQLite3::Database.new(db_new_file)
61  db.results_as_hash = true
62  sql = 'CREATE TABLE IF NOT EXISTS mew(id TEXT, path TEXT, parid TEXT, date TEXT);'
63  db.execute(sql)
64  db.transaction
65  return db
66end
67
68def close_db(db_file, db)
69  db.close
70  db_new_file = db_file + '.new'
71  if FileTest.exist?(db_new_file)
72    File.rename(db_new_file, db_file)
73  end
74end
75
76def get_ctime(db)
77  ent = db.get_first_row('SELECT * FROM mew WHERE id = ?;', '<mew-ctime>')
78  if ent
79    db.execute('DELETE FROM mew WHERE id = ?;', '<mew-ctime>' )
80    return ent['date'].to_i;
81  else
82    return 0
83  end
84end
85
86def set_ctime (db, ctime)
87  sctime = ctime.to_s
88  db.execute('INSERT INTO mew VALUES(:id, :path, :parid, :date);',
89             'id' => '<mew-ctime>', 'date' => sctime)
90end
91
92################################################################
93##
94## Fields
95##
96
97def check_id(id)
98   return nil if id == nil
99   if id =~ /\A<[-a-zA-Z0-9!#\$%&\'\*\+\/=\?\^_`{}|~\.@]+>\z/
100     return id
101   else
102     return nil
103   end
104end
105
106def get_id(msg)
107  return check_id(msg['message-id'])
108end
109
110def get_parid(msg)
111  # (1) The In-Reply-To contains one ID, use it.
112  # (2) The References contains one or more IDs, use the last one.
113  # (3) The In-Reply-To contains two or more IDs, use the first one.
114  irt = []
115  irt = msg['in-reply-to'].split(/[ \t\n]+/) if msg['in-reply-to']
116  irt.delete_if {|id| !check_id(id) }
117  return irt[0] if irt.size == 1
118  ref = []
119  ref = msg['references'].split(/[ \t\n]+/) if msg['references']
120  ref.delete_if {|id| !check_id(id) }
121  return ref.pop if ref.size > 0
122  return irt[0] if irt.size > 1
123  return nil
124end
125
126def get_date(msg)
127  begin
128    date = Time.rfc2822(msg['date']).getutc().strftime('%Y%m%d%H%M%S')
129  rescue
130    date = '19700101000000'
131  end
132  return date
133end
134
135def get_path(file)
136  # removing './'
137  return file[2..-1]
138end
139
140################################################################
141##
142##
143##
144
145def register(db, maildir, ignore_regex, target, last_mod)
146  Dir.chdir(maildir)
147  add_entry = db.prepare('INSERT INTO mew VALUES(:id, :path, :parid, :date);')
148  get_entry = db.prepare('SELECT * FROM mew WHERE id = ?;')
149  del_entry = db.prepare('DELETE FROM mew WHERE id = ? AND path = ?;')
150  db.results_as_hash = true
151  registred = 0
152  deleted = 0
153  skipdir = ''
154  begin
155    Find.find(target) do |fpath|
156      if fpath =~ ignore_regex
157        if FileTest.directory?(fpath)
158          print fpath, " (ignored)\n"
159          Find.prune # includes next
160        end
161        # next
162      else
163        st = File.lstat(fpath) rescue next
164        if st.symlink?
165          if FileTest.directory?(fpath)
166            print fpath, " (ignored)\n"
167            Find.prune # includes next
168          end
169          # next
170        elsif st.directory?
171          print fpath
172          mtime_file = File.expand_path('.mew-mtime', fpath)
173          if FileTest.file?(mtime_file) and last_mod > File.mtime(mtime_file).tv_sec
174            print " (skipped)\n"
175            skipdir = fpath
176            if st.nlink == 2
177              Find.prune # includes next
178            end
179          else
180            print "\n"
181          end
182          STDOUT.flush
183          # next
184        elsif st.file? and fpath =~ /\/[0-9]+(\.mew)?$/
185          next if File.dirname(fpath) == skipdir
186          next if last_mod > st.ctime.tv_sec
187          m = mail_header(fpath) rescue next
188          id = get_id(m)
189          parid = get_parid(m)
190          date = get_date(m)
191          path = get_path(fpath)
192          newpath = true
193          if last_mod > 0
194            get_entry.execute(id).each do |row|
195              past_path = row['path']
196              unless File.exist?(past_path)
197                del_entry.execute(id, past_path)
198                deleted = deleted + 1
199              end
200              newpath = false if path == past_path
201            end
202          end
203          if newpath == true
204            add_entry.execute('id' => id, 'path' => path, 'parid' => parid, 'date' => date)
205            registred = registred + 1
206          end
207          # next
208        end
209      end
210    end
211  ensure
212    add_entry.close
213    get_entry.close
214    del_entry.close
215    print 'Registered: ',  registred, ', deleted: ', deleted, "\n"
216  end
217end
218
219################################################################
220##
221## Main
222##
223
224require 'optparse'
225
226opts = {}
227OptionParser.new {|opt|
228  begin
229    opt.on('-f', 'full building') {|v| opts[:f] = v }
230    opt.parse!(ARGV)
231  rescue OptionParser::ParseError => e
232    STDERR.puts opt
233    exit 1
234  end
235}
236
237db_file = ARGV[0] || File.expand_path('~/Mail/id.db')
238maildir = ARGV[1] || File.expand_path('~/Mail')
239ignore_regex = Regexp.new(ARGV[2] || '^\./casket$|^\./casket/|^\./casket_replica$|^\./casket_replica/|/\.')
240target = if ARGV[3]; './' + ARGV[3] else '.' end
241have_target = if ARGV[3]; true else false end
242fullupdate = opts[:f] == true
243
244if fullupdate == true and have_target == true
245    STDERR.print "Error: -f and target_folder cannot be specified at the same time.\n"
246    exit 1
247end
248
249db = open_db(db_file, fullupdate)
250
251curr_mod = Time.now.tv_sec
252last_mod = get_ctime(db)
253comp_mod = if fullupdate == true; 0 else last_mod end
254
255begin
256  register(db, maildir, ignore_regex, target, comp_mod)
257  db.commit
258  db.execute('CREATE INDEX IF NOT EXISTS mew_id ON mew (id);')
259  db.execute('REINDEX mew_id;')
260  db.execute('CREATE INDEX IF NOT EXISTS mew_parid ON mew (parid);')
261  db.execute('REINDEX mew_parid;')
262ensure
263  if have_target == true
264    set_ctime(db, last_mod)
265  else
266    set_ctime(db, curr_mod)
267  end
268  close_db(db_file, db)
269end
270
271# Copyright (C) 2008 Mew developing team.
272# All rights reserved.
273#
274# Redistribution and use in source and binary forms, with or without
275# modification, are permitted provided that the following conditions
276# are met:
277#
278# 1. Redistributions of source code must retain the above copyright
279#    notice, this list of conditions and the following disclaimer.
280# 2. Redistributions in binary form must reproduce the above copyright
281#    notice, this list of conditions and the following disclaimer in the
282#    documentation and/or other materials provided with the distribution.
283# 3. Neither the name of the team nor the names of its contributors
284#    may be used to endorse or promote products derived from this software
285#    without specific prior written permission.
286#
287# THIS SOFTWARE IS PROVIDED BY THE TEAM AND CONTRIBUTORS ``AS IS'' AND
288# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
289# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
290# PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL THE TEAM OR CONTRIBUTORS BE
291# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
292# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
293# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
294# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
295# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
296# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
297# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
298