imap_processor 1.3 → 1.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,15 +1,17 @@
1
1
  require 'imap_processor'
2
+ require "time"
2
3
 
3
4
  ##
4
5
  # Archives old mail on IMAP server by moving it to dated mailboxen.
5
6
 
6
7
  class IMAPProcessor::Archive < IMAPProcessor
7
- attr_reader :list, :move
8
+ attr_reader :list, :move, :sep, :split
8
9
 
9
10
  def self.process_args(args)
10
11
  required_options = {
11
12
  :List => true,
12
13
  :Move => false,
14
+ :Split => false,
13
15
  }
14
16
 
15
17
  super __FILE__, args, required_options do |opts, options|
@@ -24,6 +26,17 @@ imap_archive archives old mail on IMAP server by moving it to dated mailboxen.
24
26
  opts.on("--[no-]move", "Move the messages (off by default)") do |move|
25
27
  options[:Move] = move
26
28
  end
29
+
30
+ opts.on("--[no-]split", "Split mailbox into multiple months (off by default)") do |move|
31
+ options[:Split] = move
32
+ end
33
+
34
+ opts.on("-s", "--sep SEPARATOR",
35
+ "Mailbox date separator character",
36
+ "Default: Read from ~/.#{@@opts_file_name}",
37
+ "Options file name: :Sep") do |sep|
38
+ options[:Sep] = sep
39
+ end
27
40
  end
28
41
  end
29
42
 
@@ -32,6 +45,8 @@ imap_archive archives old mail on IMAP server by moving it to dated mailboxen.
32
45
 
33
46
  @list = options[:List]
34
47
  @move = options[:Move]
48
+ @sep = options[:Sep] || '.'
49
+ @split = options[:Split]
35
50
 
36
51
  connection = connect
37
52
 
@@ -40,7 +55,7 @@ imap_archive archives old mail on IMAP server by moving it to dated mailboxen.
40
55
 
41
56
  def the_first
42
57
  t = Time.now
43
- the_first = Time.local(t.year, t.month, 1)
58
+ Time.local(t.year, t.month, 1)
44
59
  end
45
60
 
46
61
  def last_month
@@ -57,25 +72,55 @@ imap_archive archives old mail on IMAP server by moving it to dated mailboxen.
57
72
 
58
73
  def run
59
74
  @boxes.each do |mailbox|
60
- destination = "#{mailbox}.#{last_month}"
61
-
62
75
  log "SELECT #{mailbox}"
63
- response = imap.select mailbox
76
+ imap.select mailbox
64
77
 
65
- search = make_search
78
+ uids_by_date = self.uids_by_date
66
79
 
67
- log "SEARCH #{search.join ' '}"
68
- uids = imap.search search
80
+ next if uids_by_date.empty?
69
81
 
70
- next if uids.empty?
82
+ unless split then
83
+ today = Time.now
84
+ d = today - 86400 * today.day
85
+ latest = [d.year, d.month]
71
86
 
72
- puts "#{uids.length} messages in #{mailbox}#{list ? ':' : ''}"
87
+ uids_by_date = {
88
+ latest => uids_by_date.values.flatten(1)
89
+ }
90
+ end
73
91
 
74
- show_messages uids
92
+ uids_by_date.sort.each do |date, uids|
93
+ next if uids.empty?
94
+ destination = "#{mailbox}#{sep}%4d-%02d" % date
95
+ puts "#{destination}:"
96
+ puts
97
+ show_messages uids
98
+ move_messages uids, destination, false if move
99
+ end
75
100
 
76
- move_messages uids, destination if move
101
+ log "EXPUNGE"
102
+ imap.expunge
77
103
  end
78
104
  end
79
105
 
80
- end
106
+ def uids_by_date
107
+ search = make_search
108
+ log "SEARCH #{search.join ' '}"
109
+ uids = imap.search search
110
+
111
+ return {} if uids.empty?
112
+
113
+ payload = imap.fetch(uids, 'BODY.PEEK[HEADER.FIELDS (DATE)]')
114
+
115
+ mail = Hash[uids.zip(payload).map { |uid, m|
116
+ date = m.attr["BODY[HEADER.FIELDS (DATE)]"].strip.split(/:\s*/, 2).last
117
+ date = Time.parse(date) rescue Time.now
118
+ [uid, date]
119
+ }]
81
120
 
121
+ mail.keys.group_by { |uid|
122
+ date = mail[uid]
123
+ [date.year, date.month]
124
+ }
125
+ end
126
+ end
@@ -0,0 +1,67 @@
1
+ require 'imap_processor/client'
2
+
3
+ ##
4
+ # Cleanse removes old messages from your IMAP mailboxes so you don't
5
+ # have to!
6
+ #
7
+ # aka part one of my Plan for Total Email Domination.
8
+ #
9
+ # Cleanse doesn't remove messages you haven't read nor messages you've
10
+ # flagged.
11
+
12
+ class IMAPProcessor::Cleanse < IMAPProcessor::Client
13
+
14
+ ##
15
+ # Creates a new Cleanse from +options+.
16
+ #
17
+ # Options include:
18
+ # +:Age+:: Delete messages older than this many days ago
19
+ #
20
+ # and all options from IMAPClient
21
+
22
+ def initialize(options)
23
+ super
24
+
25
+ @cleanse = options[:cleanse]
26
+ @boxes = @cleanse.keys
27
+ end
28
+
29
+ def self.process_args(args)
30
+ super __FILE__, args, {} do |opts, options|
31
+ opts.banner << <<-EOF
32
+ imap_cleanse removes old messages from your IMAP mailboxes.
33
+ EOF
34
+ end
35
+ end
36
+
37
+ ##
38
+ # Removes read, unflagged messages from all selected mailboxes...
39
+
40
+ def run
41
+ super "Cleansing read, unflagged old messages",
42
+ [:Deleted] do
43
+ @imap.expunge
44
+ log "Expunged deleted messages"
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ ##
51
+ # Searches for read, unflagged messages older than :Age in the currently
52
+ # selected mailbox (see Net::IMAP#select).
53
+
54
+ def find_messages
55
+ mailbox = @boxes.find { |box| @mailbox =~ /#{box}/ } # TODO: needs more work
56
+ raise unless mailbox
57
+ age = @cleanse[mailbox]
58
+ before_date = (Time.now - 86400 * age).imapdate
59
+
60
+ search [
61
+ 'NOT', 'NEW',
62
+ 'NOT', 'FLAGGED',
63
+ 'BEFORE', before_date
64
+ ], 'read, unflagged messages'
65
+ end
66
+
67
+ end
@@ -0,0 +1,145 @@
1
+ $TESTING = false unless defined? $TESTING
2
+ # require 'net/imap'
3
+ # require 'yaml'
4
+ # require 'imap_sasl_plain'
5
+ # require 'optparse'
6
+ # require 'enumerator'
7
+
8
+ require "imap_processor"
9
+
10
+ ##
11
+ # This class only exists to transition from IMAPCleanse to imap_processor
12
+
13
+ class IMAPProcessor::Client < IMAPProcessor
14
+
15
+ ##
16
+ # Creates a new IMAPClient from +options+.
17
+ #
18
+ # Options include:
19
+ # +:Verbose+:: Verbose flag
20
+ # +:Noop+:: Don't delete anything flag
21
+ # +:Root+:: IMAP root path
22
+ # +:Boxes+:: Comma-separated list of mailbox prefixes to search
23
+ # +:Host+:: IMAP server
24
+ # +:Port+:: IMAP server port
25
+ # +:SSL+:: SSL flag
26
+ # +:Username+:: IMAP username
27
+ # +:Password+:: IMAP password
28
+ # +:Auth+:: IMAP authentication type
29
+
30
+ def initialize(options)
31
+ super
32
+
33
+ @noop = options[:Noop]
34
+ @root = options[:Root]
35
+
36
+ root = @root
37
+ root += "/" unless root.empty?
38
+
39
+ connect options[:Host], options[:Port], options[:SSL],
40
+ options[:Username], options[:Password], options[:Auth]
41
+ end
42
+
43
+ ##
44
+ # Selects messages from mailboxes then marking them with +flags+. If a
45
+ # block is given it is run after message marking.
46
+ #
47
+ # Unless :Noop was set, then it just prints out what it would do.
48
+ #
49
+ # Automatically called by IMAPClient::run
50
+
51
+ def run(message, flags)
52
+ log message
53
+
54
+ message_count = 0
55
+ mailboxes = find_mailboxes
56
+
57
+ mailboxes.each do |mailbox|
58
+ @mailbox = mailbox
59
+ @imap.select @mailbox
60
+ log "Selected #{@mailbox}"
61
+
62
+ messages = find_messages
63
+
64
+ next if messages.empty?
65
+
66
+ message_count += messages.length
67
+
68
+ unless @noop then
69
+ mark messages, flags
70
+ else
71
+ log "Noop - not marking"
72
+ end
73
+
74
+ yield messages if block_given?
75
+ end
76
+
77
+ log "Done. Found #{message_count} messages in #{mailboxes.length} mailboxes"
78
+ end
79
+
80
+ ##
81
+ # Connects to IMAP server +host+ at +port+ using ssl if +ssl+ is true then
82
+ # logs in as +username+ with +password+. IMAPClient will really only work
83
+ # with PLAIN auth on SSL sockets, sorry.
84
+
85
+ def connect(host, port, ssl, username, password, auth = nil)
86
+ @imap = Net::IMAP.new host, port, ssl, nil, false
87
+ log "Connected to #{host}:#{port}"
88
+
89
+ if auth.nil? then
90
+ auth_caps = @imap.capability.select { |c| c =~ /^AUTH/ }
91
+ raise "Couldn't find a supported auth type" if auth_caps.empty?
92
+ auth = auth_caps.first.sub(/AUTH=/, '')
93
+ end
94
+
95
+ auth = auth.upcase
96
+ log "Trying #{auth} authentication"
97
+ @imap.authenticate auth, username, password
98
+ log "Logged in as #{username}"
99
+ end
100
+
101
+ ##
102
+ # Finds mailboxes with messages that were selected by the :Boxes option.
103
+
104
+ def find_mailboxes
105
+ mailboxes = @imap.list(@root, "*")
106
+
107
+ if mailboxes.nil? then
108
+ log "Found no mailboxes under #{@root.inspect}, you may have an incorrect root"
109
+ return []
110
+ end
111
+
112
+ mailboxes.reject! { |mailbox| mailbox.attr.include? :Noselect }
113
+ mailboxes.map! { |mailbox| mailbox.name }
114
+
115
+ @box_re = /^#{Regexp.escape @root}#{Regexp.union(*@boxes)}/
116
+
117
+ mailboxes.reject! { |mailbox| mailbox !~ @box_re }
118
+ mailboxes = mailboxes.sort_by { |m| m.downcase }
119
+ log "Found #{mailboxes.length} mailboxes to search:"
120
+ mailboxes.each { |mailbox| log "\t#{mailbox}" } if @verbose
121
+ return mailboxes
122
+ end
123
+
124
+ ##
125
+ # Searches for messages matching +query+ in the selected mailbox
126
+ # (see Net::IMAP#select). Logs 'Scanning for +message+' before searching.
127
+
128
+ def search(query, message)
129
+ log " Scanning for #{message}"
130
+ messages = @imap.search query
131
+ log " Found #{messages.length} messages"
132
+ return messages
133
+ end
134
+
135
+ ##
136
+ # Marks +messages+ in the currently selected mailbox with +flags+
137
+ # (see Net::IMAP#store).
138
+
139
+ def mark(messages, flags)
140
+ messages.each_slice(500) do |chunk|
141
+ @imap.store chunk, '+FLAGS.SILENT', flags
142
+ end
143
+ log "Marked messages with flags"
144
+ end
145
+ end
@@ -0,0 +1,121 @@
1
+ require 'imap_processor/client'
2
+
3
+ ##
4
+ # Automatically flag your messages, yo!
5
+ #
6
+ # aka part two of my Plan for Total Email Domination.
7
+ #
8
+ # IMAPFlag flags messages you've responded to, messages you've written and
9
+ # messages in response to messages you've written.
10
+ #
11
+ # If you unflag a message IMAPFlag is smart and doesn't re-flag it.
12
+ #
13
+ # I chose these settings because I find these messages interesting but don't
14
+ # want to manually flag them. Why should I do all the clicking when the
15
+ # computer can do it for me?
16
+
17
+ class IMAPProcessor::Flag < IMAPProcessor::Client
18
+
19
+ ##
20
+ # IMAP keyword for automatically flagged messages
21
+
22
+ AUTO_FLAG_KEYWORD = 'IMAPFLAG_AUTO_FLAGGED'
23
+
24
+ ##
25
+ # Message-Id query
26
+
27
+ MESSAGE_ID = 'HEADER.FIELDS (MESSAGE-ID)'
28
+
29
+ ##
30
+ # Creates a new IMAPFlag from +options+.
31
+ #
32
+ # Options include:
33
+ # +:Email:: Email address used for sending email
34
+ #
35
+ # and all options from IMAPClient
36
+
37
+ def initialize(options)
38
+ super
39
+
40
+ @flag = options[:flag]
41
+ @boxes = @flag.keys
42
+ end
43
+
44
+ def self.process_args(args)
45
+ super __FILE__, args, {} do |opts, options|
46
+ opts.banner << <<-EOF
47
+ imap_flag automatically flags your messages.
48
+ EOF
49
+ end
50
+ end
51
+
52
+ ##
53
+ # Removes read, unflagged messages from all selected mailboxes...
54
+
55
+ def run
56
+ super "Flagging messages", [:Flagged, AUTO_FLAG_KEYWORD]
57
+ end
58
+
59
+ ##
60
+ # Searches for messages I answered and messages I wrote.
61
+
62
+ def find_messages
63
+ @box = @boxes.find { |box| @mailbox =~ /#{box}/ } # TODO: needs more work
64
+ raise unless @box
65
+ @email = @flag[@box]
66
+ raise unless @email
67
+ return [answered_in_curr, wrote_in_curr, responses_in_curr].flatten
68
+ end
69
+
70
+ ##
71
+ # Answered messages in the selected mailbox.
72
+
73
+ def answered_in_curr
74
+ search [
75
+ 'ANSWERED',
76
+ 'NOT', 'FLAGGED',
77
+ 'NOT', 'KEYWORD', AUTO_FLAG_KEYWORD
78
+ ], 'answered messages'
79
+ end
80
+
81
+ def all_email
82
+ @email.map { |e| "FROM #{e}" }.inject { |s,e| "OR #{s} #{e}" }
83
+ end
84
+
85
+ ##
86
+ # Messages I wrote in the selected mailbox.
87
+
88
+ def wrote_in_curr
89
+ search("#{self.all_email} NOT FLAGGED NOT KEYWORD AUTO_FLAG_KEYWORD",
90
+ "messages by #{@email.join(", ")}")
91
+ end
92
+
93
+ ##
94
+ # Messages in response to messages I wrote in the selected mailbox.
95
+
96
+ def responses_in_curr
97
+ log " Scanning for responses to messages I wrote"
98
+ my_mail = @imap.search self.all_email
99
+
100
+ return [] if my_mail.empty?
101
+
102
+ msg_ids = @imap.fetch my_mail, "BODY.PEEK[#{MESSAGE_ID}]"
103
+ msg_ids.map! do |data|
104
+ data.attr["BODY[#{MESSAGE_ID}]"].split(':', 2).last.strip
105
+ end
106
+
107
+ messages = msg_ids.map do |id|
108
+ @imap.search([
109
+ 'HEADER', 'In-Reply-To', id,
110
+ 'NOT', 'FLAGGED',
111
+ 'NOT', 'KEYWORD', AUTO_FLAG_KEYWORD
112
+ ])
113
+ end
114
+
115
+ messages.flatten!
116
+
117
+ log " Found #{messages.length} messages"
118
+
119
+ return messages
120
+ end
121
+ end