Arthur POULET
45f1efb731
I changed my email provider recently and it seems I'm less free to set the sender field now. So I cannot say this email comes from the real user, I need to set it to the mailing list email, maybe the true email behind the lists. This change allow this. - Add some documentation (I did not updated mailinglistrb since some time so I needed to read my code again to understand what it does. This documentation should help any other user when configuring there own instance. - Ignore more .env files, just in case - Improve SMTP error handler (it does not do a lot of things but ensure we log unknown smtp errors as well) - Add IMAP disconnection error management (it is still under test, I do not know for sure it fixed the issue where IMAP disconnected and not recovered. Also it is a bit clumsy as it does not ensure this at every step using the imap tcp socket. - Add a SENDER config variable to force set sender field to something else than the original sender when distributing emails. - Fix (probably ?) a bug where the port may not be taken into accound and ssl parameter neither. Did not tested it because I'm lazy. - Replace mailinglist address suffix with + rather than . because it's probably a more common way to handle alias. - Add more tasks to the Rakefile
184 lines
5.2 KiB
Ruby
184 lines
5.2 KiB
Ruby
require "net/imap"
|
|
|
|
# Protocls::Imap allows to fetch emails.
|
|
#
|
|
# https://www.rfc-editor.org/rfc/rfc3501
|
|
#
|
|
# TODO: strengthen the network management to avoid connection loss.
|
|
class Protocols::Imap
|
|
MODERATION_INBOX = "INBOX/moderation".freeze
|
|
REFUSED_INBOX = "INBOX/refused".freeze
|
|
BASE_INBOX = "INBOX".freeze
|
|
ALL_INBOXES = [MODERATION_INBOX, REFUSED_INBOX, BASE_INBOX].freeze
|
|
|
|
def initialize
|
|
reset!
|
|
end
|
|
attr_reader :imap if $debug
|
|
|
|
def reset!
|
|
@imap = Net::IMAP.new(ENV["IMAP_HOST"], port: ENV["IMAP_PORT"], ssl: true)
|
|
@imap.authenticate('PLAIN', ENV["IMAP_USER"], ENV["IMAP_PASSWORD"])
|
|
ALL_INBOXES.each do
|
|
$logger.info "Try to create inbox #{_1}"
|
|
@imap.create(_1)
|
|
$logger.info "Inbox #{_1} created"
|
|
rescue StandardError => e
|
|
$logger.error(e.message)
|
|
end # TODO: properly do that
|
|
end
|
|
|
|
# Fetch the next incomming email as a Protocols::Mail.
|
|
#
|
|
# @return [Protocols::Mail]
|
|
def fetch_next_unseen(inbox:, max_tries: 2)
|
|
return $logger.error("fetch_next_unseen reached max_tries_limit") if max_tries == 0
|
|
|
|
begin
|
|
uid = search_unseen(inbox:).first
|
|
rescue IOError => e
|
|
$logger.warn e.message
|
|
reset!
|
|
return fetch_next_unseen(inbox:, max_tries: max_tries - 1)
|
|
end
|
|
|
|
return nil if uid.nil?
|
|
fetch(uid:, inbox:)
|
|
end
|
|
|
|
# Fetch all UID that are not marked as SEEN yet in the inbox
|
|
#
|
|
# @param inbox [String]
|
|
# @return [Array(Integer)]
|
|
def search_unseen(inbox:)
|
|
goto_inbox(inbox)
|
|
@imap.uid_search(%w[NOT SEEN])
|
|
end
|
|
|
|
# Fetch the first UID of message matching with the given message_id.
|
|
# Will return nil if none are matching.
|
|
# It is possible for several messages to have the same message_id.
|
|
# In this case it is impossible to say which one will be returned.
|
|
# In practice, it is likely that the older message will be always returned.
|
|
#
|
|
# @param inbox [String]
|
|
# @return [Integer?]
|
|
def search_message_id(message_id, inbox:)
|
|
goto_inbox(inbox)
|
|
@imap.uid_search(["HEADER", "Message-ID", message_id]).first
|
|
end
|
|
|
|
# Open an given inbox. Do not multiply requests to the IMAP server.
|
|
#
|
|
# @param inbox [String] the name of the inbox repository
|
|
# @param readonly [TrueClass, FalseClass] if true, next operations won't modify the box (with move, store...)
|
|
#
|
|
# @example
|
|
#
|
|
# goto_inbox Protocols::Imap::BASE_INBOX
|
|
# goto_inbox Protocols::Imap::MODERATION_INBOX, readonly: true
|
|
def goto_inbox(inbox, readonly: false)
|
|
return if @current_inbox == inbox
|
|
|
|
$logger.debug "goto_inbox #{inbox}"
|
|
select_method = readonly ? :examine : :select
|
|
@imap.send(select_method, inbox)
|
|
@imap.check
|
|
@current_inbox = inbox
|
|
end
|
|
|
|
# Fetch the first message by UID or ID if it exists.
|
|
#
|
|
# @param id [String?] optional, specify either id or uid
|
|
# @param uid [String?] optional, specify either id or uid
|
|
#
|
|
# @return [Protocols::Mail?]
|
|
def fetch_first(inbox:, uid: nil, id: nil)
|
|
goto_inbox(inbox)
|
|
fetch_method = id ? :fetch : :uid_fetch
|
|
imap_mail = @imap.send(
|
|
fetch_method,
|
|
id || uid,
|
|
[Protocols::ENVELOPE, Protocols::UID, Protocols::HEADERS, Protocols::BODYTEXT, Protocols::MSG_ID],
|
|
)&.first
|
|
return nil if imap_mail.nil?
|
|
|
|
$logger.debug imap_mail.attr["RFC822.HEADER"]
|
|
Protocols::Mail.new(imap_mail:)
|
|
end
|
|
|
|
# Fetch one incoming mail as a Protocols::Mail.
|
|
# {see #fetch_first}
|
|
#
|
|
# @param id [String?]
|
|
# @param uid [String?]
|
|
#
|
|
# @example
|
|
#
|
|
# fetch(uid: 1, inbox: Protocols::Imap::BASE_INBOX)
|
|
#
|
|
# @return [Protocols::Mail]
|
|
def fetch(inbox:, id: nil, uid: nil)
|
|
raise "Need id OR uid to be set" if !id && !uid
|
|
|
|
uid = search_unseen(inbox:).first
|
|
mail = fetch_first(uid:, inbox:)
|
|
|
|
$logger.info "READ #{id || uid} #{mail.from}\t -> #{mail.to}:\t#{mail.subject}"
|
|
|
|
mail
|
|
end
|
|
|
|
# Move an email to the another inbox.
|
|
#
|
|
# @param inbox [String?] inbox to take the email from. Do nothing if nil.
|
|
# @param mail [Protocols::Mail] mail to move
|
|
# @param to [String] inbox repository where the mail is placed
|
|
def move(mail:, to:, inbox:)
|
|
goto_inbox inbox
|
|
$logger.info "MOVED #{mail.uid} from #{inbox} to #{to}"
|
|
@imap.uid_move mail.uid, to
|
|
end
|
|
|
|
# Move an email to the moderated inbox.
|
|
#
|
|
# @param mail [Protocols::Mail]
|
|
def moderate(mail, inbox: BASE_INBOX)
|
|
move mail:, to: MODERATION_INBOX, inbox:
|
|
end
|
|
|
|
# # Remove an email from the moderated inbox.
|
|
# #
|
|
# # @param mail [Protocols::Mail]
|
|
# def unmoderate(mail)
|
|
# @imap.uid_move mail.uid, BASE_INBOX
|
|
# end
|
|
|
|
# Add the SEEN flag to a given email.
|
|
# This email will not be reprocessed again.
|
|
#
|
|
# @param imap_mail [Protocols::Mail]
|
|
def seen!(imap_mail, inbox:)
|
|
goto_inbox inbox
|
|
$logger.info "MARK #{imap_mail.attr['UID']} as SEEN"
|
|
@imap.uid_store imap_mail.attr["UID"], "+FLAGS", [Net::IMAP::SEEN]
|
|
end
|
|
|
|
# Mark all existing messages as SEEN. See {#seen!}
|
|
def see_all_messages!
|
|
$logger.info "MARK ALL NOT SEEN as SEEN"
|
|
goto_inbox BASE_INBOX
|
|
see_all!
|
|
goto_inbox MODERATION_INBOX
|
|
see_all!
|
|
end
|
|
|
|
# :nodoc:
|
|
private def see_all!
|
|
@imap.uid_search(%w[NOT SEEN]).each do
|
|
@imap.uid_store _1, "+FLAGS", [Net::IMAP::SEEN]
|
|
$logger.debug "MARK #{_1} as SEEN"
|
|
end
|
|
end
|
|
end
|