lib/logstash/inputs/imap.rb in logstash-input-imap-3.1.0 vs lib/logstash/inputs/imap.rb in logstash-input-imap-3.2.0
- old
+ new
@@ -1,17 +1,26 @@
# encoding: utf-8
require "logstash/inputs/base"
require "logstash/namespace"
require "logstash/timestamp"
require "stud/interval"
-require "socket" # for Socket.gethostname
+require 'fileutils'
+require 'logstash/plugin_mixins/ecs_compatibility_support'
+require 'logstash/plugin_mixins/ecs_compatibility_support/target_check'
+require 'logstash/plugin_mixins/validator_support/field_reference_validation_adapter'
+
# Read mails from IMAP server
#
# Periodically scan an IMAP folder (`INBOX` by default) and move any read messages
# to the trash.
class LogStash::Inputs::IMAP < LogStash::Inputs::Base
+
+ include LogStash::PluginMixins::ECSCompatibilitySupport(:disabled, :v1, :v8 => :v1)
+
+ extend LogStash::PluginMixins::ValidatorSupport::FieldReferenceValidationAdapter
+
config_name "imap"
default :codec, "plain"
config :host, :validate => :string, :required => true
@@ -22,27 +31,61 @@
config :secure, :validate => :boolean, :default => true
config :verify_cert, :validate => :boolean, :default => true
config :folder, :validate => :string, :default => 'INBOX'
config :fetch_count, :validate => :number, :default => 50
- config :lowercase_headers, :validate => :boolean, :default => true
config :check_interval, :validate => :number, :default => 300
+
+ config :lowercase_headers, :validate => :boolean, :default => true
+
+ config :headers_target, :validate => :field_reference # ECS default: [@metadata][input][imap][headers]
+
config :delete, :validate => :boolean, :default => false
config :expunge, :validate => :boolean, :default => false
+
config :strip_attachments, :validate => :boolean, :default => false
config :save_attachments, :validate => :boolean, :default => false
- # For multipart messages, use the first part that has this
- # content-type as the event message.
+ # Legacy default: [attachments]
+ # ECS default: [@metadata][input][imap][attachments]
+ config :attachments_target, :validate => :field_reference
+
+ # For multipart messages, use the first part that has this content-type as the event message.
config :content_type, :validate => :string, :default => "text/plain"
# Whether to use IMAP uid to track last processed message
config :uid_tracking, :validate => :boolean, :default => false
# Path to file with last run time metadata
config :sincedb_path, :validate => :string, :required => false
+ def initialize(*params)
+ super
+
+ if original_params.include?('headers_target')
+ @headers_target = normalize_field_ref(headers_target)
+ else
+ # NOTE: user specified `headers_target => ''` means disable headers (@headers_target == nil)
+ # unlike our default here (@headers_target == '') causes setting headers at top level ...
+ @headers_target = ecs_compatibility != :disabled ? '[@metadata][input][imap][headers]' : ''
+ end
+
+ if original_params.include?('attachments_target')
+ @attachments_target = normalize_field_ref(attachments_target)
+ else
+ @attachments_target = ecs_compatibility != :disabled ? '[@metadata][input][imap][attachments]' : '[attachments]'
+ end
+ end
+
+ # @note a '' target value is normalized to nil
+ def normalize_field_ref(target)
+ return nil if target.nil? || target.empty?
+ # so we can later event.set("#{target}[#{name}]", ...)
+ target.match?(/\A[^\[\]]+\z/) ? "[#{target}]" : target
+ end
+ private :normalize_field_ref
+
def register
require "net/imap" # in stdlib
require "mail" # gem 'mail'
if @secure and not @verify_cert
@@ -61,18 +104,19 @@
if @sincedb_path.nil?
datapath = File.join(LogStash::SETTINGS.get_value("path.data"), "plugins", "inputs", "imap")
# Ensure that the filepath exists before writing, since it's deeply nested.
FileUtils::mkdir_p datapath
@sincedb_path = File.join(datapath, ".sincedb_" + Digest::MD5.hexdigest("#{@user}_#{@host}_#{@port}_#{@folder}"))
+ @logger.debug? && @logger.debug("Generated sincedb path", sincedb_path: @sincedb_path)
end
- if File.directory?(@sincedb_path)
- raise ArgumentError.new("The \"sincedb_path\" argument must point to a file, received a directory: \"#{@sincedb_path}\"")
- end
- @logger.info("Using \"sincedb_path\": \"#{@sincedb_path}\"")
+ @logger.info("Using", sincedb_path: @sincedb_path)
if File.exist?(@sincedb_path)
+ if File.directory?(@sincedb_path)
+ raise ArgumentError.new("The \"sincedb_path\" argument must point to a file, received a directory: \"#{@sincedb_path}\"")
+ end
@uid_last_value = File.read(@sincedb_path).to_i
- @logger.info("Loading \"uid_last_value\": \"#{@uid_last_value}\"")
+ @logger.debug? && @logger.debug("Loaded from sincedb", uid_last_value: @uid_last_value)
end
@content_type_re = Regexp.new("^" + @content_type)
end # def register
@@ -134,20 +178,19 @@
end
rescue => e
@logger.error("Encountered error #{e.class}", :message => e.message, :backtrace => e.backtrace)
# Do not raise error, check_mail will be invoked in the next run time
-
ensure
# Close the connection (and ignore errors)
imap.close rescue nil
imap.disconnect rescue nil
# Always save @uid_last_value so when tracking is switched from
# "NOT SEEN" to "UID" we will continue from first unprocessed message
if @uid_last_value
- @logger.info("Saving \"uid_last_value\": \"#{@uid_last_value}\"")
+ @logger.debug? && @logger.debug("Saving to sincedb", uid_last_value: @uid_last_value)
File.write(@sincedb_path, @uid_last_value)
end
end
def parse_attachments(mail)
@@ -162,11 +205,12 @@
return attachments
end
def parse_mail(mail)
# Add a debug message so we can track what message might cause an error later
- @logger.debug? && @logger.debug("Working with message_id", :message_id => mail.message_id)
+ @logger.debug? && @logger.debug("Processing mail", message_id: mail.message_id)
+
# TODO(sissel): What should a multipart message look like as an event?
# For now, just take the plain-text part and set it as the message.
if mail.parts.count == 0
# No multipart message, just use the body as the event text
message = mail.body.decoded
@@ -181,57 +225,58 @@
@codec.decode(message) do |event|
# Use the 'Date' field as the timestamp
event.timestamp = LogStash::Timestamp.new(mail.date.to_time)
- # Add fields: Add message.header_fields { |h| h.name=> h.value }
- mail.header_fields.each do |header|
- # 'header.name' can sometimes be a Mail::Multibyte::Chars, get it in String form
- name = @lowercase_headers ? header.name.to_s.downcase : header.name.to_s
- # Call .decoded on the header in case it's in encoded-word form.
- # Details at:
- # https://github.com/mikel/mail/blob/master/README.md#encodings
- # http://tools.ietf.org/html/rfc2047#section-2
- value = transcode_to_utf8(header.decoded.to_s)
+ process_headers(mail, event) if @headers_target
- # Assume we already processed the 'date' above.
- next if name == "Date"
-
- case (field = event.get(name))
- when String
- # promote string to array if a header appears multiple times
- # (like 'received')
- event.set(name, [field, value])
- when Array
- field << value
- event.set(name, field)
- when nil
- event.set(name, value)
- end
- end
-
# Add attachments
- if attachments && attachments.length > 0
- event.set('attachments', attachments)
+ if attachments && attachments.length > 0 && @attachments_target
+ event.set(@attachments_target, attachments)
end
decorate(event)
event
end
end
+ def process_headers(mail, event)
+ # Add fields: Add message.header_fields { |h| h.name=> h.value }
+ mail.header_fields.each do |header|
+ # 'header.name' can sometimes be a Mail::Multibyte::Chars, get it in String form
+ name = header.name.to_s
+ name = name.downcase if @lowercase_headers
+
+ # Call .decoded on the header in case it's in encoded-word form.
+ # Details at:
+ # https://github.com/mikel/mail/blob/master/README.md#encodings
+ # http://tools.ietf.org/html/rfc2047#section-2
+ value = transcode_to_utf8(header.decoded)
+
+ targeted_name = "#{@headers_target}[#{name}]"
+ case (field = event.get(targeted_name))
+ when String
+ # promote string to array if a header appears multiple times (like 'received')
+ event.set(targeted_name, [field, value])
+ when Array
+ field << value
+ event.set(targeted_name, field)
+ when nil
+ event.set(targeted_name, value)
+ end
+ end
+ end
+
def stop
Stud.stop!(@run_thread)
- $stdin.close
end
private
# transcode_to_utf8 is meant for headers transcoding.
# the mail gem will set the correct encoding on header strings decoding
# and we want to transcode it to utf8
def transcode_to_utf8(s)
- unless s.nil?
- s.encode(Encoding::UTF_8, :invalid => :replace, :undef => :replace)
- end
+ return nil if s.nil?
+ s.encode(Encoding::UTF_8, :invalid => :replace, :undef => :replace)
end
end