Skip to content

Commit e1ada62

Browse files
prehorrobbavey
authored andcommitted
Use IMAP uid to retrieve new mails instead of "NOT SEEN" flag
This commit introduces new configuration options: * `uid_tracking` - set to `true` to use IMAP uid instead of "NOT SEEN" flag * `sincedb_path` - path to file with last processed IMAP uid. Defaults to `#{path.data}/plugins/input/imap/.sincedb_#{Digest::MD5.hexdigest("#{@user}_#{@host}_#{@PORT}_#{@folder}")}` IMAP uid is always stored in the file `sincedb_path` regardles of the `uid_tracking` setting, so we can switch between "NOT SEEN" and "UID" tracking. In transition from the previous plugin version, we first need to process at least one mail with `uid_tracking` set to `false` to save the last processed IMAP uid and then switch `uid_tracking` to `true`. Fixes #36
1 parent 487dd0d commit e1ada62

File tree

2 files changed

+42
-7
lines changed

2 files changed

+42
-7
lines changed

lib/logstash/inputs/imap.rb

100644100755
Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ class LogStash::Inputs::IMAP < LogStash::Inputs::Base
3434
# content-type as the event message.
3535
config :content_type, :validate => :string, :default => "text/plain"
3636

37+
# Whether to use IMAP uid to track last processed message
38+
config :uid_tracking, :validate => :boolean, :default => true
39+
40+
# Path to file with last run time metadata
41+
config :sincedb_path, :validate => :string, :required => false
42+
3743
def register
3844
require "net/imap" # in stdlib
3945
require "mail" # gem 'mail'
@@ -50,6 +56,22 @@ def register
5056
end
5157
end
5258

59+
# Load last processed IMAP uid from file if exists
60+
if @sincedb_path.nil?
61+
datapath = File.join(LogStash::SETTINGS.get_value("path.data"), "plugins", "inputs", "imap")
62+
# Ensure that the filepath exists before writing, since it's deeply nested.
63+
FileUtils::mkdir_p datapath
64+
@sincedb_path = File.join(datapath, ".sincedb_" + Digest::MD5.hexdigest("#{@user}_#{@host}_#{@port}_#{@folder}"))
65+
end
66+
if File.directory?(@sincedb_path)
67+
raise ArgumentError.new("The \"sincedb_path\" argument must point to a file, received a directory: \"#{@sincedb_path}\"")
68+
end
69+
@logger.info("Using \"sincedb_path\": \"#{@sincedb_path}\"")
70+
if File.exist?(@sincedb_path)
71+
@uid_last_value = File.read(@sincedb_path)
72+
@logger.info("Loading \"uid_last_value\": \"#{@uid_last_value}\"")
73+
end
74+
5375
@content_type_re = Regexp.new("^" + @content_type)
5476
end # def register
5577

@@ -75,10 +97,18 @@ def check_mail(queue)
7597
# EOFError, OpenSSL::SSL::SSLError
7698
imap = connect
7799
imap.select(@folder)
78-
ids = imap.search("NOT SEEN")
100+
if @uid_tracking && @uid_last_value
101+
# If there are no new messages, uid_search returns @uid_last_value
102+
# because it is the last message, so we need to delete it.
103+
ids = imap.uid_search(["UID", (@uid_last_value..-1)]).delete_if { |uid|
104+
uid <= @uid_last_value
105+
}
106+
else
107+
ids = imap.uid_search("NOT SEEN")
108+
end
79109

80110
ids.each_slice(@fetch_count) do |id_set|
81-
items = imap.fetch(id_set, "RFC822")
111+
items = imap.uid_fetch(id_set, ["RFC822", "UID"])
82112
items.each do |item|
83113
next unless item.attr.has_key?("RFC822")
84114
mail = Mail.read_from_string(item.attr["RFC822"])
@@ -87,16 +117,21 @@ def check_mail(queue)
87117
else
88118
queue << parse_mail(mail)
89119
end
120+
@uid_last_value = item.attr["UID"]
90121
end
91122

123+
@logger.debug? && @logger.debug("Saving \"uid_last_value\": \"#{@uid_last_value}\"")
124+
# Always save @uid_last_value so when tracking is switched from
125+
# "NOT SEEN" to "UID" we will continue from first unprocessed message
126+
File.write(@sincedb_path, @uid_last_value) unless @uid_last_value.nil?
92127
imap.store(id_set, '+FLAGS', @delete ? :Deleted : :Seen)
93-
94128
end
95129

96-
# Enable an 'expunge' IMAP command after the items.each loop
130+
# Enable an 'expunge' IMAP command after the items.each loop
97131
if @expunge
98-
# Force messages to be marked as "Deleted", the above may or may not be working as expected. "Seen" means nothing if you are going to
99-
# delete a message after processing.
132+
# Force messages to be marked as "Deleted", the above may or may not be
133+
# working as expected. "Seen" means nothing if you are going to delete
134+
# a message after processing.
100135
imap.store(id_set, '+FLAGS', [:Deleted])
101136
imap.expunge()
102137
end

spec/inputs/imap_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
allow(imap).to receive(:store)
2626
allow(ids).to receive(:each_slice).and_return([])
2727

28-
allow(imap).to receive(:search).with("NOT SEEN").and_return(ids)
28+
allow(imap).to receive(:uid_search).with("NOT SEEN").and_return(ids)
2929
allow(Net::IMAP).to receive(:new).and_return(imap)
3030
end
3131
end

0 commit comments

Comments
 (0)