We recently had the opportunity to create a note in Evernote through their Evernote API. It was a little different from using other APIs because Evernote uses Thrift instead of XML/JSON-RPC like many other web applications.

As with any integration, there are a few gotchas with this one. In particular, we found the Ruby-oriented documentation on the web a little lacking. Other than that, here were the major challenges:

Without further ado, below is the full source code:

require 'oauth'
require 'evernote-thrift'


#this is for testing.  in production, it's www.evernote.com
base = "https://sandbox.evernote.com" 
# given to you by Evernote
key = "developer-key-from-evernote" 
# given to you by Evernote
secret = "developer-secret-from-evernote" 
# needs to be a valid path in your application
callback = "http://www.pollen.io/evernote-oauth-callback" 
oauth = OAuth::Consumer.new( key,secret,{:site=>base,
                      :authorize_path => "/OAuth.action",
                      :access_token_path=>"/oauth",
                      :request_token_path=>"/oauth"})
#this lets you see raw wire calls
oauth.http.set_debug_output($stdout) 

#this callback is not ignored and is actually used
request_token = oauth.get_request_token(:oauth_callback=>callback) 
puts request_token.inspect

#pretend like the user logged in and pushed the "Authorize" button
oauth_verifier = pretend_to_authorize_as_user(request_token)
#end of pretending like the user logged in

#now, finally, get the access_token you wanted and then use it to your heart's content
access_token = request_token.get_access_token(:oauth_verifier=>oauth_verifier)
puts access_token.inspect

# you need to store the url for the note store when you get it in the access_token.
#this can differ by user, so don't just use the same one for everybody
note_store_url = access_token.params['edam_noteStoreUrl']
access_token_str = access_token.token

#Here is where we create an actual note.
#  This will all feel familiar for someone with a Java background, but if you've
#   only ever used Ruby it's going to feel really weird.
note = Evernote::EDAM::Type::Note.new
my_content = "Hello, this is a note from Pollen.io.  Please check us out if you need enterprise-grade cloud integration"
#This is a special gotcha.  The content of your note needs to be wrapped in this special xml
note.content = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><!DOCTYPE en-note SYSTEM \"http://xml.evernote.com/pub/enml2.dtd\"><en-note><![CDATA[#{my_content}]]></en-note>"
note.title = "I love Evernote"
note.tagNames = ['pollen.io','evernote']
noteStoreTransport = Thrift::HTTPClientTransport.new(note_store_url)
noteStoreProtocol = Thrift::BinaryProtocol.new(noteStoreTransport)
noteStore = Evernote::EDAM::NoteStore::NoteStore::Client.new(noteStoreProtocol)     
begin 
  note = noteStore.createNote(access_token_str, note)
rescue Evernote::EDAM::Error::EDAMUserException => e
  #the exceptions that come back from Evernote are hard to read, but really important to keep track of
  msg = "Caught an exception from Evernote trying to create a note.  #{translate_error(e)}"
  raise msg
end      

We omitted a few functions above for clarity, so here they are for completeness sake.

First, the function to simulate a user being redirected to Evernote, logging in, and pushing the “Authorize” button. Of course, in a real app, you’d actually redirect a real user there instead, but as I said we like to simulate this in integration tests.


def store_cookies(response)
  rv = {}
  cookies = response.get_fields("Set-Cookie")
  if cookies
    cookies.each do |cookie|
      real_cookie = cookie.split('; ')[0]
      key, value = real_cookie.split("=")
      rv[key] = real_cookie #yes, setting the whole cookie and not just the value
    end
  end
  rv
end


#here we are pretending to be the user and submitting the login form as if we were them.  In reality, you'd redirect
# the user to the url and then wait for Freshbooks to redirect them back to you.
def pretend_to_authorize_as_user(request_token)
  net = Net::HTTP.new("sandbox.evernote.com", 443)
  net.use_ssl = true
  net.verify_mode = OpenSSL::SSL::VERIFY_NONE
  net.set_debug_output $stdout 
  net.read_timeout = 5
  net.open_timeout = 5
  
  #force a session to start
  # this doesn't seem to work unless you have a session
  start_request = Net::HTTP::Post.new("https://sandbox.evernote.com/Login.action")
  start_response = net.start do |http|
     http.request(start_request)
  end
  cookies = store_cookies(start_response)
  #extract the session id from the cookies
  session_id = cookies['JSESSIONID'].split('=')[1]
  
  #now login as if you were that user
  login_params = {:username=>'a-test-user-name',
                  :password=>'a-test-user-password',
                  :login=>'Sign In',
                  :targetUrl=>CGI.escape("/OAuth.action?oauth_token=#{request_token.token}")}
  login_request = Net::HTTP::Post.new("https://sandbox.evernote.com/Login.action;jsessionid=#{session_id}")
  login_request.set_form_data(login_params)
  login_request.add_field("Cookie", cookies.values.join("; "))
  login_request.add_field("Cookie", "JSESSIONID=#{@session_id}") if @session_id
  login_response = net.start do |http|
     http.request(login_request)
  end
  cookies = cookies.merge(store_cookies(login_response))
  
  #now actually push the "Authorize" button
  authorize_params = {:authorize=>"Authorize", :oauth_token=>request_token.token}
  authorize_request = Net::HTTP::Post.new("https://sandbox.evernote.com/OAuth.action")
  authorize_request.set_form_data(authorize_params)
  authorize_request.add_field("Cookie", cookies.values.join("; "))
  authorize_request.add_field("Cookie", "JSESSIONID=#{@session_id}") if @session_id
  authorize_response = net.start do |http|
     http.request(authorize_request)
  end
  #now, finally, extract what we came here for in the first place, the access token
  location = authorize_response['location'].scan(/oauth_verifier=(\d*\w*)/)
  oauth_verifier = location[0][0]
  oauth_verifier
end

Next, our error handling code, so you can see how to understand what happened when something goes wrong:

# see: http://www.ruby-doc.org/gems/docs/e/evernote-1.2.0/Evernote/EDAM/Error/EDAMErrorCode.html
# see: http://www.ruby-doc.org/gems/docs/e/evernote-1.2.0/Evernote/EDAM/Error/EDAMUserException.html
def translate_error(e)
  error_name = "unknown"
  case e.errorCode
  when Evernote::EDAM::Error::EDAMErrorCode::AUTH_EXPIRED
    error_name = "AUTH_EXPIRED"
  when Evernote::EDAM::Error::EDAMErrorCode::BAD_DATA_FORMAT
    error_name = "BAD_DATA_FORMAT"
  when Evernote::EDAM::Error::EDAMErrorCode::DATA_CONFLICT
    error_name = "DATA_CONFLICT"
  when Evernote::EDAM::Error::EDAMErrorCode::DATA_REQUIRED
    error_name = "DATA_REQUIRED"
  when Evernote::EDAM::Error::EDAMErrorCode::ENML_VALIDATION
    error_name = "ENML_VALIDATION"
  when Evernote::EDAM::Error::EDAMErrorCode::INTERNAL_ERROR
    error_name = "INTERNAL_ERROR"
  when Evernote::EDAM::Error::EDAMErrorCode::INVALID_AUTH
    error_name = "INVALID_AUTH"
  when Evernote::EDAM::Error::EDAMErrorCode::LIMIT_REACHED
    error_name = "LIMIT_REACHED"
  when Evernote::EDAM::Error::EDAMErrorCode::PERMISSION_DENIED
    error_name = "PERMISSION_DENIED"
  when Evernote::EDAM::Error::EDAMErrorCode::QUOTA_REACHED
    error_name = "QUOTA_REACHED"
  when Evernote::EDAM::Error::EDAMErrorCode::SHARD_UNAVAILABLE
    error_name = "SHARD_UNAVAILABLE"
  when Evernote::EDAM::Error::EDAMErrorCode::UNKNOWN
    error_name = "UNKNOWN"
  when Evernote::EDAM::Error::EDAMErrorCode::VALID_VALUES
    error_name = "VALID_VALUES"
  when Evernote::EDAM::Error::EDAMErrorCode::VALUE_MAP
    error_name = "VALUE_MAP"
  end
  rv = "Error code was: #{error_name}[#{e.errorCode}] and parameter: [#{e.parameter}]"  
end

[UPDATE]: Anja Skrba of Croatia has translated this page into Serbo-Croatian. Thanks Anja!