imap_processor 1.3 → 1.5

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.
@@ -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