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