From 65f87822fccf34d0440fe78fe92000f4674934fa Mon Sep 17 00:00:00 2001 From: Cris Ward Date: Tue, 29 Aug 2017 23:00:02 +0100 Subject: [PATCH] first commit --- .editorconfig | 7 +++ .gitignore | 8 +++ .travis.yml | 1 + LICENSE | 21 +++++++ README.md | 41 ++++++++++++++ shard.yml | 9 +++ spec/imap_spec.cr | 17 ++++++ spec/spec_helper.cr | 2 + src/imap.cr | 131 ++++++++++++++++++++++++++++++++++++++++++++ src/imap/version.cr | 3 + 10 files changed, 240 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE create mode 100644 README.md create mode 100644 shard.yml create mode 100644 spec/imap_spec.cr create mode 100644 spec/spec_helper.cr create mode 100644 src/imap.cr create mode 100644 src/imap/version.cr diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..8f0c87a --- /dev/null +++ b/.editorconfig @@ -0,0 +1,7 @@ +[*.cr] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +indent_style = space +indent_size = 2 +trim_trailing_whitespace = true diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..23ec656 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +/doc/ +/lib/ +/bin/ +/.shards/ + +# Libraries don't need dependency lock +# Dependencies will be locked in application that uses them +/shard.lock diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..ffc7b6a --- /dev/null +++ b/.travis.yml @@ -0,0 +1 @@ +language: crystal diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..866672e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Cris Ward + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d55cd58 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +# imap + +A very much WIP imap library for crystal. + +## Installation + +Add this to your application's `shard.yml`: + +```yaml +dependencies: + imap: + github: crisward/imap +``` + +## Usage + +```crystal +require "imap" + +imap = Imap::Client.new(host: "imap.gmail.com", port: 993, username: "email@gmail.com", password: "*******") +mailboxes = imap.get_mailboxes +if mailboxes.size > 0 + mailbox = mailboxes[0] + imap.set_mailbox(mailbox) + message_count = imap.get_message_count + puts "There are #{message_count} message in #{mailbox}" +end +imap.close +``` + +## Contributing + +1. Fork it ( https://github.com/crisward/imap/fork ) +2. Create your feature branch (git checkout -b my-new-feature) +3. Commit your changes (git commit -am 'Add some feature') +4. Push to the branch (git push origin my-new-feature) +5. Create a new Pull Request + +## Contributors + +- [crisward](https://github.com/crisward) Cris Ward - creator, maintainer diff --git a/shard.yml b/shard.yml new file mode 100644 index 0000000..9bff89c --- /dev/null +++ b/shard.yml @@ -0,0 +1,9 @@ +name: imap +version: 0.1.0 + +authors: + - Cris Ward + +crystal: 0.23.1 + +license: MIT diff --git a/spec/imap_spec.cr b/spec/imap_spec.cr new file mode 100644 index 0000000..cd7ffb9 --- /dev/null +++ b/spec/imap_spec.cr @@ -0,0 +1,17 @@ +require "./spec_helper" + +describe Imap do + # TODO: Write tests + + it "should count emails in mailbox" do + imap = Imap::Client.new(host: "imap.gmail.com", port: 993, username: "***", password: "***") + mailboxes = imap.get_mailboxes + if mailboxes.size > 0 + mailbox = mailboxes[0] + imap.set_mailbox(mailbox) + message_count = imap.get_message_count + puts "There are #{message_count} message in #{mailbox}" + end + imap.close + end +end diff --git a/spec/spec_helper.cr b/spec/spec_helper.cr new file mode 100644 index 0000000..55e2465 --- /dev/null +++ b/spec/spec_helper.cr @@ -0,0 +1,2 @@ +require "spec" +require "../src/imap" diff --git a/src/imap.cr b/src/imap.cr new file mode 100644 index 0000000..2c8676b --- /dev/null +++ b/src/imap.cr @@ -0,0 +1,131 @@ +require "./imap/*" +require "openssl" +require "logger" + +module Imap + class Client + @socket : TCPSocket | OpenSSL::SSL::Socket::Client | Nil = nil + @logger : Logger + @mailbox : String? + + def initialize(host = "imap.gmail.com", port = 993, username = "", password = "", loglevel = Logger::ERROR) + @logger = Logger.new(STDOUT) + @logger.level = loglevel + @mailboxes = [] of String + @mailbox = nil + + @command_history = [] of String + @socket = TCPSocket.new(host, port) + tls_socket = OpenSSL::SSL::Socket::Client.new(@socket.as(TCPSocket), sync_close: true, hostname: host) + tls_socket.sync = false + @socket = tls_socket + login(username, password) + # list headers + # process_mail_headers(command("tag FETCH 1:#{count} (BODY[HEADER])")) + end + + private def socket + if _socket = @socket + _socket + else + raise "Client socket not opened." + end + end + + private def command(command : String, parameter : String? = nil) + command_and_parameter = command + command_and_parameter += " " + parameter if parameter + @command_history << command_and_parameter + @logger.info "=====> #{command_and_parameter}" + socket << command_and_parameter << "\r\n" + socket.flush + response + end + + private def login(username, password) + command("tag login #{username} #{password}") + end + + # sets the current mailbox + def set_mailbox(mailbox) + @mailbox = mailbox + command("tag SELECT #{mailbox}") + end + + # Returns an array of mailbox names + def get_mailboxes : Array(String) + mailboxes = [] of String + res = command(%{tag LIST "" "*"}) + res.each do |line| + if line =~ /HasNoChildren/ + name = line.match(/"([^"]+)"$/) + mailboxes << name[1].to_s if name + end + end + return mailboxes + end + + # Returns the number of messages in the current mailbox + def get_message_count + mailbox = @mailbox + if !mailbox + raise "No Mailbox set" + end + res = command("tag STATUS #{mailbox} (MESSAGES)") + # eg (MESSAGES 3) + res.each do |line| + if line =~ /MESSAGES/ + match = line.match(/MESSAGES ([0-9]+)/) + if match + return match[1].to_i + end + end + end + return 0 + end + + private def process_mail_headers(res) + ip = nil + from = nil + res.each do |line| + if line =~ /^From:/ + from = line.sub(/^From: /, "") + end + if line =~ /^Received:/ + ips = line.match(/\[(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\]/) + if ips + ip = ips[1].to_s + end + end + if ip && from + @logger.info "from: #{from} ip: #{ip}" + from = nil + ip = nil + end + end + end + + private def response + status_messages = [] of String + while (line = socket.gets) + @command_history << line + if line =~ /^\*/ + status_messages << line + elsif line =~ /^tag OK/ + status_messages << line + break + elsif line =~ /^tag (BAD|NO)/ + raise "Invalid responce \"#{line}\" received." + else + status_messages << line + end + end + status_messages + end + + # Closes the imap connection + def close + command("tag LOGOUT") + end + end +end diff --git a/src/imap/version.cr b/src/imap/version.cr new file mode 100644 index 0000000..a3875e1 --- /dev/null +++ b/src/imap/version.cr @@ -0,0 +1,3 @@ +module Imap + VERSION = "0.1.0" +end