Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ruby Implementation of JMAP-Samples #33

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions ruby/hello_world.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env ruby

require "./tiny_jmap_client"

# Set up our client from the environment and set our account ID
client = TinyJMAPClient.new(ENV["JMAP_HOSTNAME"] || "api.fastmail.com", ENV["JMAP_USERNAME"], ENV["JMAP_TOKEN"])

account_id = client.account_id

# Here, we're going to find our drafts mailbox, by calling Mailbox/query
query_res = client.jmap_call(
[["Mailbox/query", {accountId: account_id, filter: {name: "Drafts"}}, "a"]],
["urn:ietf:params:jmap:mail"]
)

# Pull out the id from the list response, and make sure we got it

draft_mailbox_id = query_res["methodResponses"][0][1]["ids"][0]

# Great! Now we're going to set up the data for the email we're going to send.
body = <<~EOF
Hi!

This email may not look like much, but I sent it with JMAP, a protocol
designed to make it easier to manage email, contacts, calendars, and more of
your digital life in general.

Pretty cool, right?

--
This email sent from my next-generation email system at Fastmail.
EOF

draft = {
from: [{email: client.username}],
to: [{email: client.username}],
subject: "Hello, world!",
keywords: {"$draft": true},
mailboxIds: {draft_mailbox_id => true},
bodyValues: {body: {value: body, charset: "utf-8"}},
textBody: [{partId: "body", type: "text/plain"}]
}

identity_id = client.identity_id

# Here, we make two calls in a single request. The first is an Email/set, to
# set our draft in our drafts folder, and the second is an
# EmailSubmission/set, to actually send the mail to ourselves. This requires
# an additional capability for submission.
method_calls = []
method_calls << ["Email/set", {accountId: account_id, create: {draft: draft}}, "a"]
method_calls << ["EmailSubmission/set",
{
accountId: account_id,
onSuccessDestroyEmail: ["#sendIt"],
create: {sendIt: {emailId: "#draft", identityId: identity_id}}
},
"b"]
create_res = client.jmap_call(method_calls, ["urn:ietf:params:jmap:mail", "urn:ietf:params:jmap:submission"])
puts(create_res)
96 changes: 96 additions & 0 deletions ruby/tiny_jmap_client.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
require "json"
require "net/http"
require "net/https"

class TinyJMAPClient
attr_reader :username, :hostname, :token
def initialize(hostname, username, token)
if hostname.nil? || username.nil? || token.nil? || hostname.empty? || username.empty? || token.empty?
raise ArgumentError("hostname, username, and token must be supplied as non nil, non empty strings")
end

@hostname = hostname
@username = username
@token = token
end

def session
@session ||= fetch_session("https://#{@hostname}/.well-known/jmap")
end

def api_url
session["apiUrl"]
end

def account_id
# Return the accountId for the account matching self.username
@account_id ||= session["primaryAccounts"]["urn:ietf:params:jmap:mail"]
end

def identity_res
@identity_res ||= jmap_call([["Identity/get", {accountId: account_id}, "i"]], ["urn:ietf:params:jmap:submission"])
end

def all_identity_ids
identity_res["methodResponses"][0][1]["list"].map { |user| {email: user["email"], id: user["id"]} }
end

def identity_id(email = nil)
# Return the identityId for an address matching @username or email if supplied
if email.nil?
email = @username
end
acct = identity_res["methodResponses"][0][1]["list"].select { |user| user["email"].downcase == email.downcase }
if acct.any?
acct[0]["id"]
end
end

def jmap_call(method_calls, using = [])
if !using.include?("urn:ietf:params:jmap:core")
using.unshift("urn:ietf:params:jmap:core")
end
uri = URI(api_url)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
body = JSON.dump({using: using, methodCalls: method_calls})
req = Net::HTTP::Post.new(uri)
req.add_field "Authorization", "Bearer #{@token}"
req.add_field "Content-Type", "application/json"
req.body = body
resp = http.request(req)
JSON.parse(resp.body)
rescue => e
puts "#{e.class}: #{e.message}"
if defined?(resp)
puts "Response code: #{resp.code}"
puts resp.body
end
end

private

def fetch_session(uri_str, limit = 10)
# This was extracted to its own method because net:http doesn't have a built in mechanism for handling redirects
# There are more user friendly http clients for Ruby, but I am using net:http to minimize external dependencies
if limit < 1
raise StandardError.new("Too many redirects")
end
uri = URI(uri_str)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.verify_mode = OpenSSL::SSL::VERIFY_PEER
req = Net::HTTP::Get.new(uri)
req.add_field "Authorization", "Bearer #{@token}"
req.add_field "Content-Type", "application/json"
resp = http.request(req)
if resp["location"]
fetch_session(resp["location"], limit - 1)
elsif resp.code == "200"
JSON.parse(resp.body)
else
raise StandardError.new(resp.msg)
end
end
end
38 changes: 38 additions & 0 deletions ruby/top-ten.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
#!/usr/bin/env ruby

require "./tiny_jmap_client"

client = TinyJMAPClient.new(ENV["JMAP_HOSTNAME"] || "api.fastmail.com", ENV["JMAP_USERNAME"], ENV["JMAP_TOKEN"])
account_id = client.account_id

inbox_res = client.jmap_call(
[["Mailbox/query", {accountId: account_id, filter: {role: "inbox", hasAnyRole: true}}, "a"]],
["urn:ietf:params:jmap:mail"]
)
inbox_id = inbox_res["methodResponses"][0][1]["ids"][0]

get_res = client.jmap_call(
[
[
"Email/query",
{
accountId: account_id,
filter: {inMailbox: inbox_id},
sort: [{property: "receivedAt", isAscending: false}], limit: 10
},
"a"
],
[
"Email/get",
{
accountId: account_id,
properties: ["id", "subject", "receivedAt"],
"#ids": {resultOf: "a", name: "Email/query", path: "/ids/*"}
},
"b"
]
],
["urn:ietf:params:jmap:mail"]
)

get_res["methodResponses"][1][1]["list"].each { |email| puts "#{email["receivedAt"]} - #{email["subject"]}" }