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