This commit is contained in:
Yaroslav Sidlovsky 2018-09-10 22:53:48 +03:00
parent 3de478f8ba
commit 6c39ea859b
4 changed files with 82 additions and 30 deletions

View file

@ -5,11 +5,10 @@ describe Imap do
it "should count emails in mailbox" do it "should count emails in mailbox" do
imap = Imap::Client.new(host: "imap.gmail.com", port: 993, username: "***", password: "***") imap = Imap::Client.new(host: "imap.gmail.com", port: 993, username: "***", password: "***")
mailboxes = imap.get_mailboxes mailboxes = imap.list
if mailboxes.size > 0 if mailboxes.size > 0
mailbox = mailboxes[0] mailbox = mailboxes.first
imap.set_mailbox(mailbox) message_count = imap.status(mailbox, ["MESSAGES"])["MESSAGES"]
message_count = imap.get_message_count
puts "There are #{message_count} message in #{mailbox}" puts "There are #{message_count} message in #{mailbox}"
end end
imap.close imap.close

View file

@ -1,12 +1,16 @@
require "./imap/*"
require "openssl" require "openssl"
require "logger" require "logger"
require "./imap/*"
module Imap module Imap
class Client class Client
CAPS_UTF8 = ImmutableSet.new("UTF8=ALL", "UTF8=ONLY", "UTF8=ACCEPT")
@caps : Set(String) @caps : Set(String)
@socket : TCPSocket | OpenSSL::SSL::Socket::Client | Nil = nil @socket : TCPSocket | OpenSSL::SSL::Socket::Client | Nil = nil
@logger : Logger @logger : Logger
@idling = false
def initialize(host = "imap.gmail.com", port = 993, username = "", password = "", loglevel = Logger::ERROR) def initialize(host = "imap.gmail.com", port = 993, username = "", password = "", loglevel = Logger::ERROR)
@logger = Logger.new(STDERR) @logger = Logger.new(STDERR)
@ -16,11 +20,13 @@ module Imap
tls_socket = OpenSSL::SSL::Socket::Client.new(@socket.as(TCPSocket), sync_close: true, hostname: host) tls_socket = OpenSSL::SSL::Socket::Client.new(@socket.as(TCPSocket), sync_close: true, hostname: host)
tls_socket.sync = false tls_socket.sync = false
@socket = tls_socket @socket = tls_socket
login(username, password) login(username, password)
# list headers
# process_mail_headers(command("tag FETCH 1:#{count} (BODY[HEADER])"))
@caps = capability @caps = capability
if CAPS_UTF8.intersects? @caps
command("ENABLE", "UTF8=ACCEPT")
end
end end
private def socket private def socket
@ -31,22 +37,20 @@ module Imap
end end
end end
private def command(command : String, *parameters, &block : String -> Bool) private def send(data : String)
command_and_parameter = case command socket << data << "\r\n"
when "DONE" socket.flush
command @logger.debug "Sent: #{data}"
else end
"tag #{command}"
end
private def command(command : String, *parameters, &block : String -> Bool)
command_and_parameter = "tag #{command}"
if parameters.size > 0 if parameters.size > 0
params = parameters.join(" ") params = parameters.join(" ")
command_and_parameter += " #{params}" command_and_parameter += " #{params}"
end end
socket << command_and_parameter << "\r\n" send command_and_parameter
socket.flush
@logger.debug "Sent: #{command_and_parameter}"
while (line = socket.gets) while (line = socket.gets)
@logger.debug " Got: #{line}" @logger.debug " Got: #{line}"
@ -64,7 +68,7 @@ module Imap
res << line res << line
next false next false
elsif line =~ /^tag (BAD|NO)/ elsif line =~ /^tag (BAD|NO)/
raise "Invalid responce \"#{line}\" received." raise "Invalid response \"#{line}\" received."
else else
res << line res << line
end end
@ -75,7 +79,7 @@ module Imap
end end
private def login(username, password) private def login(username, password)
command("login", username, password) command("LOGIN", username, password)
end end
# Sends a CAPABILITY command # Sends a CAPABILITY command
@ -92,20 +96,24 @@ module Imap
spawn do spawn do
command("IDLE") do |line| command("IDLE") do |line|
if line =~ /\+ idling/ if line =~ /\+ idling/
# nothing to do @idling = true
elsif line =~ /\* (\d+) ([A-Z]+)/ elsif line =~ /\* (\d+) ([A-Z]+)/
block.call($2, $1.to_u32) block.call($2, $1.to_u32)
else else
@idling = false
next false next false
end end
true true
end end
end end
Fiber.yield
end end
# Sends a DONE command # Sends a DONE command
def done def idle_done
command("DONE") raise "IDLE not started" unless @idling
send("DONE")
end end
# Sends a SELECT command to select a +mailbox+ so that messages # Sends a SELECT command to select a +mailbox+ so that messages
@ -134,7 +142,7 @@ module Imap
# Returns an array of mailbox names # Returns an array of mailbox names
def list : Set(String) def list : Array(String)
mailboxes = [] of String mailboxes = [] of String
res = command(%{LIST "" "*"}) res = command(%{LIST "" "*"})
res.each do |line| res.each do |line|
@ -143,8 +151,7 @@ module Imap
mailboxes << name[1].to_s if name mailboxes << name[1].to_s if name
end end
end end
# TODO: decode MIME encoded UTF-8 strings return mailboxes
return Set.new(mailboxes)
end end
# Sends a STATUS command, and returns the status of the indicated # Sends a STATUS command, and returns the status of the indicated
@ -198,7 +205,7 @@ module Imap
# Closes the imap connection # Closes the imap connection
def close def close
command("LOGOUT") rescue nil command("LOGOUT") rescue nil
@socket.close @socket.not_nil!.close if @socket
end end
end end
end end

45
src/imap/immutable_set.cr Normal file
View file

@ -0,0 +1,45 @@
class ImmutableSet(T)
@set : Set(T)
def initialize(*params : T)
@set = Set.new(params)
end
def each
@set.each
end
def size
@set.size
end
def to_s(io)
io << "ImmutableSet{"
join ", ", io, &.inspect(io)
io << '}'
end
def empty?
@set.empty?
end
def inspect(io)
to_s(io)
end
def object_id
@set.object_id
end
def includes?(obj)
@set.includes?(obj)
end
def intersects?(other)
@set.intersects?(other)
end
def pretty_print(pp) : Nil
pp.list("ImmutableSet{", @set, "}")
end
end

View file

@ -10,13 +10,14 @@ end
imap = Imap::Client.new(host: "imap.gmail.com", port: 993, username: ARGV[0], password: ARGV[1], loglevel: Logger::DEBUG) imap = Imap::Client.new(host: "imap.gmail.com", port: 993, username: ARGV[0], password: ARGV[1], loglevel: Logger::DEBUG)
mailboxes = imap.list mailboxes = imap.list
if mailboxes.includes?("INBOX") if mailboxes.includes?("INBOX")
pp imap.status("INBOX", ["MESSAGES", "RECENT"])
imap.select("INBOX") imap.select("INBOX")
imap.idle do |name, value| imap.idle do |name, value|
puts "#{name} => #{value}" puts "#{name} => #{value}"
end end
end end
while true sleep 3
sleep 5 imap.idle_done
imap.done imap.close
end