summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore8
-rw-r--r--fake-service/.rspec3
-rw-r--r--fake-service/.ruby-gemset1
-rw-r--r--fake-service/.ruby-version1
-rw-r--r--fake-service/Gemfile16
-rw-r--r--fake-service/Gemfile.lock115
-rw-r--r--fake-service/README.md2
-rw-r--r--fake-service/Rakefile52
-rw-r--r--fake-service/config.ru4
-rw-r--r--fake-service/data/mail-sets/keep0
-rw-r--r--fake-service/data/tags513
-rw-r--r--fake-service/features/compose_save_draft_and_send.feature15
-rw-r--r--fake-service/features/forward_trash_archive.feature12
-rw-r--r--fake-service/features/search_and_destroy.feature11
-rw-r--r--fake-service/features/step_definitions/compose.rb27
-rw-r--r--fake-service/features/step_definitions/mail_list.rb55
-rw-r--r--fake-service/features/step_definitions/mail_view.rb67
-rw-r--r--fake-service/features/step_definitions/search.rb12
-rw-r--r--fake-service/features/step_definitions/tag_list.rb15
-rw-r--r--fake-service/features/support/env.rb31
-rw-r--r--fake-service/features/tag_and_reply.feature12
-rwxr-xr-xfake-service/go3
-rw-r--r--fake-service/lib/generator.rb96
-rw-r--r--fake-service/lib/smail.rb27
-rw-r--r--fake-service/lib/smail/all.rb8
-rw-r--r--fake-service/lib/smail/combined_observer.rb23
-rw-r--r--fake-service/lib/smail/contact.rb26
-rw-r--r--fake-service/lib/smail/contacts.rb84
-rw-r--r--fake-service/lib/smail/contacts_observer.rb54
-rw-r--r--fake-service/lib/smail/contacts_search.rb71
-rw-r--r--fake-service/lib/smail/contacts_sorter.rb8
-rw-r--r--fake-service/lib/smail/fake.rb146
-rw-r--r--fake-service/lib/smail/mail.rb299
-rw-r--r--fake-service/lib/smail/mail_scope_filter.rb70
-rw-r--r--fake-service/lib/smail/mail_service.rb152
-rw-r--r--fake-service/lib/smail/mailset.rb84
-rw-r--r--fake-service/lib/smail/paginate.rb15
-rw-r--r--fake-service/lib/smail/paginated_enumerable.rb29
-rw-r--r--fake-service/lib/smail/persona.rb12
-rw-r--r--fake-service/lib/smail/search.rb133
-rw-r--r--fake-service/lib/smail/search/and_match.rb25
-rw-r--r--fake-service/lib/smail/search/negate_match.rb22
-rw-r--r--fake-service/lib/smail/search/or_match.rb25
-rw-r--r--fake-service/lib/smail/search/scope_match.rb79
-rw-r--r--fake-service/lib/smail/search/string_match.rb37
-rw-r--r--fake-service/lib/smail/search/true_match.rb13
-rw-r--r--fake-service/lib/smail/security_casing.rb55
-rw-r--r--fake-service/lib/smail/security_casing_examples.rb142
-rw-r--r--fake-service/lib/smail/server.rb82
-rw-r--r--fake-service/lib/smail/sorted_mail.rb57
-rw-r--r--fake-service/lib/smail/stats.rb60
-rw-r--r--fake-service/lib/smail/stats_observer.rb19
-rw-r--r--fake-service/lib/smail/tags.rb150
-rw-r--r--fake-service/spec/fixtures/mail115
-rw-r--r--fake-service/spec/fixtures/mail216
-rw-r--r--fake-service/spec/smail/mail_spec.rb103
-rw-r--r--fake-service/spec/spec_helper.rb1
57 files changed, 3213 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
index ad24fc04..72b859d0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,14 @@ app/bower_components
dist/
*archive.zip
*.swp
+*.swo
app/js/generated
test-results.xml
control_tower.html
+state.yml
+.server.pid
+*archive.zip
+artifacts/
+public/
+.DS_Store
+screenshot*
diff --git a/fake-service/.rspec b/fake-service/.rspec
new file mode 100644
index 00000000..2559e39f
--- /dev/null
+++ b/fake-service/.rspec
@@ -0,0 +1,3 @@
+--color
+--format progress
+--require spec_helper
diff --git a/fake-service/.ruby-gemset b/fake-service/.ruby-gemset
new file mode 100644
index 00000000..28b34978
--- /dev/null
+++ b/fake-service/.ruby-gemset
@@ -0,0 +1 @@
+fake-smail-back
diff --git a/fake-service/.ruby-version b/fake-service/.ruby-version
new file mode 100644
index 00000000..eca07e4c
--- /dev/null
+++ b/fake-service/.ruby-version
@@ -0,0 +1 @@
+2.1.2
diff --git a/fake-service/Gemfile b/fake-service/Gemfile
new file mode 100644
index 00000000..d7ecedc8
--- /dev/null
+++ b/fake-service/Gemfile
@@ -0,0 +1,16 @@
+source 'https://rubygems.org'
+
+gem "rake"
+gem "sinatra"
+gem "sinatra-contrib"
+gem "json"
+gem "faker"
+gem "shotgun"
+gem "rakeup"
+gem "rspec"
+gem "mail"
+
+gem "capybara"
+gem "selenium-webdriver"
+gem "cucumber"
+gem "capybara-screenshot"
diff --git a/fake-service/Gemfile.lock b/fake-service/Gemfile.lock
new file mode 100644
index 00000000..f6695218
--- /dev/null
+++ b/fake-service/Gemfile.lock
@@ -0,0 +1,115 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ addressable (2.3.6)
+ backports (3.6.0)
+ builder (3.2.2)
+ capybara (2.3.0)
+ mime-types (>= 1.16)
+ nokogiri (>= 1.3.3)
+ rack (>= 1.0.0)
+ rack-test (>= 0.5.4)
+ xpath (~> 2.0)
+ capybara-screenshot (0.3.19)
+ capybara (>= 1.0, < 3)
+ launchy
+ childprocess (0.5.3)
+ ffi (~> 1.0, >= 1.0.11)
+ cucumber (1.3.15)
+ builder (>= 2.1.2)
+ diff-lcs (>= 1.1.3)
+ gherkin (~> 2.12)
+ multi_json (>= 1.7.5, < 2.0)
+ multi_test (>= 0.1.1)
+ diff-lcs (1.2.5)
+ faker (1.3.0)
+ i18n (~> 0.5)
+ ffi (1.9.3)
+ ffi (1.9.3-java)
+ gherkin (2.12.2)
+ multi_json (~> 1.3)
+ gherkin (2.12.2-java)
+ multi_json (~> 1.3)
+ i18n (0.6.9)
+ json (1.8.1)
+ json (1.8.1-java)
+ launchy (2.4.2)
+ addressable (~> 2.3)
+ launchy (2.4.2-java)
+ addressable (~> 2.3)
+ spoon (~> 0.0.1)
+ mail (2.5.4)
+ mime-types (~> 1.16)
+ treetop (~> 1.4.8)
+ mime-types (1.25.1)
+ mini_portile (0.6.0)
+ multi_json (1.10.1)
+ multi_test (0.1.1)
+ nokogiri (1.6.2.1)
+ mini_portile (= 0.6.0)
+ nokogiri (1.6.2.1-java)
+ polyglot (0.3.4)
+ rack (1.5.2)
+ rack-protection (1.5.3)
+ rack
+ rack-test (0.6.2)
+ rack (>= 1.0)
+ rake (10.3.2)
+ rakeup (1.2.0)
+ rack (~> 1.5.0)
+ rake (~> 10.3.0)
+ rspec (2.14.1)
+ rspec-core (~> 2.14.0)
+ rspec-expectations (~> 2.14.0)
+ rspec-mocks (~> 2.14.0)
+ rspec-core (2.14.8)
+ rspec-expectations (2.14.5)
+ diff-lcs (>= 1.1.3, < 2.0)
+ rspec-mocks (2.14.6)
+ rubyzip (1.1.4)
+ selenium-webdriver (2.42.0)
+ childprocess (>= 0.5.0)
+ multi_json (~> 1.0)
+ rubyzip (~> 1.0)
+ websocket (~> 1.0.4)
+ shotgun (0.9)
+ rack (>= 1.0)
+ sinatra (1.4.5)
+ rack (~> 1.4)
+ rack-protection (~> 1.4)
+ tilt (~> 1.3, >= 1.3.4)
+ sinatra-contrib (1.4.2)
+ backports (>= 2.0)
+ multi_json
+ rack-protection
+ rack-test
+ sinatra (~> 1.4.0)
+ tilt (~> 1.3)
+ spoon (0.0.4)
+ ffi
+ tilt (1.4.1)
+ treetop (1.4.15)
+ polyglot
+ polyglot (>= 0.3.1)
+ websocket (1.0.7)
+ xpath (2.0.0)
+ nokogiri (~> 1.3)
+
+PLATFORMS
+ java
+ ruby
+
+DEPENDENCIES
+ capybara
+ capybara-screenshot
+ cucumber
+ faker
+ json
+ mail
+ rake
+ rakeup
+ rspec
+ selenium-webdriver
+ shotgun
+ sinatra
+ sinatra-contrib
diff --git a/fake-service/README.md b/fake-service/README.md
new file mode 100644
index 00000000..269f0df0
--- /dev/null
+++ b/fake-service/README.md
@@ -0,0 +1,2 @@
+fake-smail-back
+===============
diff --git a/fake-service/Rakefile b/fake-service/Rakefile
new file mode 100644
index 00000000..3c936d1f
--- /dev/null
+++ b/fake-service/Rakefile
@@ -0,0 +1,52 @@
+$: << "lib"
+
+require 'rubygems'
+require 'rakeup'
+require 'smail'
+require 'rake/packagetask'
+
+RakeUp::ServerTask.new do |t|
+ t.port = ENV['RACK_PORT'].nil? ? 4567 : ENV['RACK_PORT'].to_i
+ t.host = '0.0.0.0'
+ t.rackup_file = "config.ru"
+ t.server = :webrick
+ t.pid_file = ".server.pid"
+end
+
+namespace :server do
+ desc "Reloading server"
+ task :reloading do
+ sh "shotgun"
+ end
+end
+
+
+desc "Console"
+task :console do
+ require 'irb'
+ require 'irb/completion'
+ ARGV.clear
+ IRB.start
+end
+
+task :default => :server
+
+namespace :mailset do
+ desc "Create new mailset"
+ task :create, :name, :num, :with_tagging do |t, args|
+ name = args[:name] || "newmailset"
+ num = (args[:num] || "10").to_i
+ tagging = args[:with_tagging] == "true"
+
+ ms = Smail::Mailset.create name, num, tagging
+ ms.save!
+ end
+end
+
+desc "Package"
+Rake::PackageTask.new("archive", :noversion) do |p|
+ p.need_zip = true
+ p.package_dir = "artifacts"
+ p.package_files.include("**/*")
+end
+
diff --git a/fake-service/config.ru b/fake-service/config.ru
new file mode 100644
index 00000000..cadabecc
--- /dev/null
+++ b/fake-service/config.ru
@@ -0,0 +1,4 @@
+$: << "lib"
+
+require 'smail'
+run Smail::Server
diff --git a/fake-service/data/mail-sets/keep b/fake-service/data/mail-sets/keep
new file mode 100644
index 00000000..e69de29b
--- /dev/null
+++ b/fake-service/data/mail-sets/keep
diff --git a/fake-service/data/tags b/fake-service/data/tags
new file mode 100644
index 00000000..d22d8c93
--- /dev/null
+++ b/fake-service/data/tags
@@ -0,0 +1,513 @@
+1d
+accessories
+Accounts
+agsta_fashion
+agua
+all_pixs
+all_sunsets
+amazing
+animals
+announcement
+API
+architecture
+Area51
+art
+asia
+australia
+autumn
+baby
+background
+band
+barcelona
+barrym
+bd
+beach
+beautiful
+beauty
+bepopular
+berlin
+bestoftheday
+bestphoto
+bestpicture
+bestsnaps
+Beta
+bike
+bird
+birds
+birthday
+black
+blackandwhite
+blogoverflow
+blondehair
+blue
+blueskys
+boots
+boy
+brogues
+brownhair
+Business
+bw
+california
+canada
+canon
+capelli
+car
+careers
+cat
+cc-wiki-dump
+changelog
+chaos
+chicago
+china
+christmas
+church
+Church
+cielo
+city
+classy
+clothes
+clothing
+cloud
+clouds
+cloudscape
+cloudstagram
+cloudy
+clubsocial
+color
+comment
+community
+concert
+CondoAssociation
+contestday
+contestgram
+cool
+coolhair
+cosmetics
+dance
+dark
+day
+dayshots
+de
+denver
+design
+devdays
+dog
+dress
+england
+est
+europe
+event
+eyeliner
+eyeshadow
+fabshots
+fabskyshots
+fall
+family
+Family
+fashion
+fashionable
+fashionblogger
+fashiondiaries
+fashionstudy
+fashionstyle
+festival
+film
+florida
+flower
+flowers
+follow
+followme
+food
+football
+footwear
+Forwards
+france
+Fraternity
+friends
+fun
+gang_family
+garden
+genginsapgan
+geo
+geotagged
+germany
+girl
+girly
+graffiti
+gramoftheday
+greatfeeds
+green
+hadoop
+hair
+haircolor
+haircut
+hairdo
+hairdye
+hairposts
+hairstyle
+hairstyles
+halloween
+hår
+hawaii
+heavenly
+heels
+highheels
+hike
+hiking
+holiday
+HomeAndLoans
+Hometown
+horizon
+hot
+hot_shotz
+house
+ic_landscapes
+ic_skies
+ic_water
+igaddicts
+ig_bestever
+igcentric_nature
+igdaily
+igers
+igersoftheday
+ig_exquisite
+igfotogram
+ignation
+ignaturale
+igscout
+ig_snapshots
+ig_sunsetshots
+ig_universal
+ig_watchers
+iloveclouds
+imagin8
+implus_daily
+india
+inspiration
+instaaaaah
+instabeach
+instabeauty
+instacool
+insta_crew
+instadaily
+instafamous
+instafashion
+instagain
+instagallery
+insta_global
+instago
+instagold
+instagramapp
+instagramer
+instagramhub
+instagroove
+instahair
+instahub
+instalike
+instalove
+instalovers
+instamillion
+instamood
+instanature
+instanaturelover
+instapic
+insta_pick_skyart
+instapicture
+instapicturing
+instaplus
+instasg
+instashoes
+insta_shot
+insta_shutter
+instasky
+insta_sleep
+instasummer
+instasunsets
+instatalent
+instatext
+instaweb
+internationalization
+iphone
+iphoneography
+irox_skyline
+irox_water
+isea_sunsets
+iskygram
+iskyhub
+island
+italia
+italy
+jacket
+japan
+jj
+kids
+kittens
+la
+lake
+landscape
+landscape_lovers
+landscapes
+lastnight
+latenight
+latergram
+legal
+life
+light
+lipgloss
+lipstick
+literature
+live
+london
+longhair
+look
+love
+loveit
+lovethem
+luna
+lunar
+mac
+macro
+makeup
+makeupartist
+mascara
+me
+meetups
+men
+message
+messages
+meta
+mexico
+mobile
+moda
+mode
+model
+moderators
+moon
+most_deserving
+mountain
+mountains
+museum
+music
+musthave
+myfav
+nagellack
+naglar
+nail
+nailartaddict
+nailartaddicts
+nailartclub
+nailartheaven
+nailartjunkie
+nailcare
+naildesign
+naildesigns
+nailpolish
+nails
+nailvarnish
+natur
+nature
+naturegram
+naturelover
+naturelovers
+nature_seekers
+natureshot
+natureza
+new
+newyork
+newyorkcity
+night
+nightsky
+nighttime
+nikon
+nite
+noche
+note
+nuture
+nyc
+ocean
+oceano
+old
+onda
+ootd
+orange
+Orders
+original
+outfit
+outfitoftheday
+pants
+paris
+park
+PartTimeJob
+party
+peak
+people
+performance
+Personal
+photo
+photoaday
+photoglobe
+photography
+photolocker
+photomafia
+photooftheday
+photos
+photowall
+pickoftheday
+picoftheday
+platforms
+podcasts
+Politics
+popularpic
+portrait
+prettynails
+primeshots
+Project
+promotion
+quote
+quotes
+raw
+read
+reading
+readit
+Recipe
+red
+redsky
+reference
+ripple
+ripples
+river
+riverbend
+rivers
+riverside
+riverwalk
+rock
+rscpics
+san
+sand
+sandals
+sanfrancisco
+scenery
+scenicsunset
+scotland
+sea
+search
+seascape
+seascapes
+seaside
+seattle
+server
+serverfault.com
+shirt
+shoe
+shoes
+shoesoftheday
+show
+sky
+skyart
+sky_love
+skylovers
+skyscapes_gf
+skysnappers
+skystyles
+skystyles_gf
+snow
+sol
+spain
+spring
+square
+squareformat
+stackexchange
+stackoverflow
+star
+stars
+statigram
+stilettos
+street
+streetwear
+style
+summer
+summit
+sun
+sunlight
+sunny
+sunrays
+sunset
+sunsethunter
+sunset_lovee
+sunsetlovers
+sunset_lovers
+sunset_pics
+sunsetsniper
+sunshine
+sunshotz
+sunspotters
+superuser.com
+survey
+swag
+swagg
+swagger
+tagsta
+tagsta_
+tagsta_fashion
+tagstagramers
+tagsta_nature
+taiwan
+tbt
+teamrebel
+texas
+text
+textgram
+textmessage
+thailand
+thecloudchasers
+themoon
+thesky
+thestars
+tokyo
+travel
+Travel
+tree
+trees
+trip
+tshirt
+tweegram
+twilightscapes
+twlight
+twlightscapes
+typography
+udog_earth
+uk
+uñas
+Uncategorized
+unitedstates
+University
+uploader
+urban
+usa
+vacation
+versagram
+video
+view
+vintage
+walking
+washington
+water
+wave
+waves
+Website
+webstagram
+wedding
+weheartit
+_wg
+white
+wilderness
+winter
+woman
+word
+words
+writer
+writing
+xulrunner
+yellow
+yolo
+zoo
diff --git a/fake-service/features/compose_save_draft_and_send.feature b/fake-service/features/compose_save_draft_and_send.feature
new file mode 100644
index 00000000..d05db837
--- /dev/null
+++ b/fake-service/features/compose_save_draft_and_send.feature
@@ -0,0 +1,15 @@
+Feature: compose mail, save draft and send mail
+
+ @wip
+ Scenario: user composes and email, save the draft, later sends the draft and checks the sent message
+ Given I compose a message with
+ | subject | body |
+ | Smail rocks! | You should definitely use it. Cheers, User. |
+ And for the 'To' field I type 'ab' and chose the first contact that shows
+ And I save the draft
+ When I open the saved draft and send it
+ Then I see that mail under the 'sent' tag
+ When I open that mail
+ Then I see that the subject reads 'Smail rocks!'
+ And I see that the body reads 'You should definitely use it. Cheers, User.'
+
diff --git a/fake-service/features/forward_trash_archive.feature b/fake-service/features/forward_trash_archive.feature
new file mode 100644
index 00000000..999d0b3c
--- /dev/null
+++ b/fake-service/features/forward_trash_archive.feature
@@ -0,0 +1,12 @@
+Feature: forward_trash_archive
+ Scenario: User forwards a mail, add CC and BCC address, later trash and archive the mail
+ When I open the first mail in the 'inbox'
+ Then I choose to forward this mail
+ And for the 'CC' field I type 'ab' and chose the first contact that shows
+ And for the 'Bcc' field I type 'fu' and chose the first contact that shows
+ And I forward this mail
+ When I open the first mail in the 'sent'
+ Then I see the mail has a cc and a bcc recipient
+ And I remove all tags
+ And I choose to trash
+ Then I see that mail under the 'trash' tag
diff --git a/fake-service/features/search_and_destroy.feature b/fake-service/features/search_and_destroy.feature
new file mode 100644
index 00000000..4a41c3f8
--- /dev/null
+++ b/fake-service/features/search_and_destroy.feature
@@ -0,0 +1,11 @@
+Feature: search html mail and destroy
+
+ Scenario: User searches for a mail and deletes it
+ When I search for a mail with the words "this is a html mail"
+ When I open the first mail in the mail list
+ Then I see one or more mails in the search results
+ Then I see if the mail has html content
+ When I try to delete the first mail
+ # Then I learn that the mail was deleted
+ When I select the tag 'trash'
+ Then the deleted mail is there
diff --git a/fake-service/features/step_definitions/compose.rb b/fake-service/features/step_definitions/compose.rb
new file mode 100644
index 00000000..e92d16f6
--- /dev/null
+++ b/fake-service/features/step_definitions/compose.rb
@@ -0,0 +1,27 @@
+Given /^I compose a message with$/ do |table|
+ find('#compose-mails-trigger').click
+ data = table.hashes.first
+ fill_in('Subject', with: data['subject'])
+ fill_in('Body', with: data['body'])
+end
+
+Given /^for the '(.*)' field I type '(.*)' and chose the first contact that shows$/ do |recipients_field, to_type|
+ recipients_field.downcase!
+ within("#recipients-#{recipients_field}-area") do
+ find('.tt-input').native.send_keys(to_type)
+ sleep 1
+ first('.tt-dropdown-menu div div').click
+ end
+end
+
+Given /^I save the draft$/ do
+ click_button("Save Draft")
+end
+
+When /^I open the saved draft and send it$/ do
+ step "I select the tag 'drafts'"
+ step "I open the first mail in the mail list"
+ page.should_not have_css("#send-button[disabled]")
+ click_button('Send')
+ find('#user-alerts').should have_content("Your message was sent!")
+end
diff --git a/fake-service/features/step_definitions/mail_list.rb b/fake-service/features/step_definitions/mail_list.rb
new file mode 100644
index 00000000..d41a62a0
--- /dev/null
+++ b/fake-service/features/step_definitions/mail_list.rb
@@ -0,0 +1,55 @@
+When(/^I open the first mail in the '(.*)'$/) do |tag|
+ page.execute_script("window.scrollBy(0, -200)")
+ step "I select the tag '#{tag}'"
+ step 'I open the first mail in the mail list'
+end
+
+When(/^I open the first mail in the mail list$/) do
+ within('#mail-list') do
+ mail_link = first('a')
+ @current_mail_id = mail_link.native.attribute('href').scan(/\/(\d+)$/).flatten.first
+ begin
+ mail_link.click
+ rescue # in Chrome, the 'a' in mail_list is not clickable because it's hidden inside the 'li'
+ mail_link_parent_li = mail_link.find(:xpath, '../..')
+ mail_link_parent_li.click
+ end
+ end
+end
+
+When(/I see that mail under the '(.*)' tag/) do |tag|
+ step "I select the tag '#{tag}'"
+ check_current_mail_is_visible
+end
+
+And(/^I open the mail I previously tagged$/) do
+ open_current_mail
+end
+
+When(/^I open that mail$/) do
+ open_current_mail
+end
+
+Then(/^I see the mail I sent$/) do
+ check_current_mail_is_visible
+end
+
+Then(/^the deleted mail is there$/) do
+ check_current_mail_is_visible
+end
+
+def open_current_mail
+ within('#mail-list') do
+ begin
+ first("#mail-#{@current_mail_id} a").click
+ rescue # in Chrome, the 'a' in mail_list is not clickable because it's hidden inside the 'li'
+ first("#mail-#{@current_mail_id}").click
+ end
+ end
+end
+
+def check_current_mail_is_visible
+ within('#mail-list') do
+ have_selector?("#mail-#{@current_mail_id}").should be_true
+ end
+end
diff --git a/fake-service/features/step_definitions/mail_view.rb b/fake-service/features/step_definitions/mail_view.rb
new file mode 100644
index 00000000..19ca5736
--- /dev/null
+++ b/fake-service/features/step_definitions/mail_view.rb
@@ -0,0 +1,67 @@
+A_MAIL = /[^\s@]+@[^\s@]+\.[^\s@]+/
+
+Then(/^I see the mail has a cc and a bcc recipient$/) do
+ within('.msg-header') do
+ first('.cc').text.should =~ A_MAIL
+ first('.bcc').text.should =~ A_MAIL
+ end
+end
+
+Then(/^that email has the '(.*)' tag$/) do |tag|
+ within('#mail-view') do |e|
+ all('.tagsArea .tag').map(&:text).map(&:downcase).to_a.should include(tag)
+ end
+end
+
+When(/I add the tag '(.*)' to that mail/) do |tag|
+ page.execute_script("$('#new-tag-button').click();")
+ page.execute_script("$('#new-tag-input').val('#{tag}');")
+ find('#new-tag-input').native.send_keys [:return]
+end
+
+And(/^I reply to it$/) do
+ click_button('Reply')
+ click_button('Send')
+end
+
+Then(/^I choose to forward this mail$/) do
+ click_button('Forward')
+end
+
+Then(/^I forward this mail$/) do
+ click_button('Send')
+end
+
+
+Then(/^I remove all tags$/) do
+ within('.tagsArea') do
+ all('.tag').each do |tag|
+ tag.click
+ end
+ end
+end
+
+Then(/^I choose to trash$/) do
+ click_button('Trash message')
+end
+
+When(/^I try to delete the first mail$/) do
+ step 'I open the first mail in the mail list'
+ within('#mail-view') do
+ page.driver.find_css('#view-more-actions')[0].click
+ page.driver.execute_script("$('#delete-button-top').click();")
+ end
+ find('#user-alerts').text.should == 'Your message was moved to trash!'
+end
+
+Then(/^I see that the subject reads '(.*)'$/) do |expected_subject|
+ find('#mail-view .subject').text.should == expected_subject
+end
+
+Then(/^I see that the body reads '(.*)'$/) do |expected_body|
+ find('#mail-view .bodyArea').text.should == expected_body
+end
+
+Then(/^I see if the mail has html content/) do
+ find('#mail-view .bodyArea').should have_css('h2[style*=\'color: #3f4944\']', :text => "cborim")
+end
diff --git a/fake-service/features/step_definitions/search.rb b/fake-service/features/step_definitions/search.rb
new file mode 100644
index 00000000..de89759c
--- /dev/null
+++ b/fake-service/features/step_definitions/search.rb
@@ -0,0 +1,12 @@
+When(/^I search for a mail with the words "(.*)"$/) do |search_term|
+ search_field = find('#search-trigger input[type="search"]').native
+ search_field.send_keys(search_term)
+ search_field.send_keys(:return)
+end
+
+Then(/^I see one or more mails in the search results$/) do
+ within('#mail-list') do
+ all('li').length.should >= 1
+ end
+end
+
diff --git a/fake-service/features/step_definitions/tag_list.rb b/fake-service/features/step_definitions/tag_list.rb
new file mode 100644
index 00000000..678f5ce9
--- /dev/null
+++ b/fake-service/features/step_definitions/tag_list.rb
@@ -0,0 +1,15 @@
+When(/^I select the tag '(.*)'$/) do |tag|
+ wait_for_user_alert_to_disapear # in Chrome, the 'flash message is on top of the toggle
+ first('.left-off-canvas-toggle').click
+ page.execute_script("window.scrollBy(0, -200)")
+ within('#tag-list') { find('li', text: /#{tag}/i).click }
+end
+
+def wait_for_user_alert_to_disapear
+ begin
+ while find('#user-alerts')
+ sleep 0.1
+ end
+ rescue #if it couldn't find it, go ahead
+ end
+end
diff --git a/fake-service/features/support/env.rb b/fake-service/features/support/env.rb
new file mode 100644
index 00000000..932f259d
--- /dev/null
+++ b/fake-service/features/support/env.rb
@@ -0,0 +1,31 @@
+require 'capybara'
+require 'capybara-screenshot'
+require 'capybara-screenshot/cucumber'
+
+RACK_PORT = ENV['RACK_PORT'] || '4567'
+HOST = "http://localhost:#{RACK_PORT}"
+
+Capybara.register_driver :selenium_chrome do |app|
+ Capybara::Selenium::Driver.new(app, :browser => :chrome)
+end
+
+Capybara::Screenshot.register_driver(:selenium_chrome) do |driver, path|
+ driver.browser.save_screenshot(path)
+end
+
+driver = ENV['CUCUMBER_DRIVER'] ? ENV['CUCUMBER_DRIVER'].to_sym : :selenium_chrome
+
+Capybara.configure do |config|
+ config.run_server = false
+ config.default_driver = driver
+ config.app_host = HOST
+end
+
+include Capybara::DSL
+
+Before do
+ `curl -d '' #{HOST}/control/mailset/mediumtagged/load`
+ sleep 3
+ visit '/?lang=en'
+ page.driver.browser.manage.window.maximize
+end
diff --git a/fake-service/features/tag_and_reply.feature b/fake-service/features/tag_and_reply.feature
new file mode 100644
index 00000000..cd9c7aad
--- /dev/null
+++ b/fake-service/features/tag_and_reply.feature
@@ -0,0 +1,12 @@
+Feature: tagging and replying
+ Scenario: User tags a mail, replies to it then checks that mail is in the right tag
+ When I open the first mail in the 'inbox'
+ Then that email has the 'inbox' tag
+ When I add the tag 'website' to that mail
+ Then I see that mail under the 'website' tag
+ And I open the mail I previously tagged
+ And I reply to it
+ When I select the tag 'sent'
+ Then I see the mail I sent
+
+
diff --git a/fake-service/go b/fake-service/go
new file mode 100755
index 00000000..0c810510
--- /dev/null
+++ b/fake-service/go
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+rake $*
diff --git a/fake-service/lib/generator.rb b/fake-service/lib/generator.rb
new file mode 100644
index 00000000..35518844
--- /dev/null
+++ b/fake-service/lib/generator.rb
@@ -0,0 +1,96 @@
+require 'faker'
+
+I18n.enforce_available_locales = true
+Faker::Config.locale = 'en-us'
+
+module Generator
+ TAGS = File.read(File.join(File.dirname(__FILE__), "..", "data", "tags")).split.map { |tt| tt.chomp }
+ module Mail
+ def random_header
+ {
+ from: Faker::Internet.email,
+ to: Faker::Internet.email,
+ subject: Faker::Lorem.sentence
+ }
+ end
+
+ def random_body
+ Faker::Lorem.paragraphs.join("\n\n")
+ end
+
+ extend Mail
+ end
+
+ def tag
+ TAGS.sample
+ end
+
+ def ladder_distribution(from, to, factor = 1)
+ mid = from + ((to - from) / 2)
+ result = []
+ curr = 1
+ direction = 1
+ (from..to).each do |i|
+ result.concat [i] * curr
+ if i == mid
+ direction = -1
+ end
+ curr += (direction * factor)
+ end
+ result
+ end
+
+ def choose(distribution)
+ case distribution
+ when Integer
+ distribution
+ when Range
+ rand((distribution.last+1) - distribution.first) + distribution.first
+ when Array
+ choose(distribution.sample)
+ end
+ end
+
+ def tags(distribution = 3)
+ num = choose(distribution)
+ num.times.map { self.tag }
+ end
+
+ def random_mail
+ hdr = Mail.random_header
+ bd = Mail.random_body
+ Smail::Mail.new(
+ from: hdr[:from],
+ to: hdr[:to],
+ subject: hdr[:subject],
+ body: bd
+ )
+ end
+
+ def random_tagged_mail(tagset)
+ hdr = Mail.random_header
+ bd = Mail.random_body
+ tgs = choose(ladder_distribution(1, 5)).times.map { tagset.sample }.uniq
+ special_tag = ([nil, nil, nil, nil, nil, nil] + Smail::Tags::SPECIAL).sample
+ status = []
+ status << :read if special_tag == 'sent'
+ mail = Smail::Mail.new(
+ from: hdr[:from],
+ to: hdr[:to],
+ subject: hdr[:subject],
+ body: bd,
+ tags: (tgs + Array(special_tag)).compact,
+ status: status
+ )
+ mail
+ end
+
+ def random_persona
+ Smail::Persona.new(Faker::Number.number(10),
+ Faker::Name.name,
+ Faker::Lorem.sentence,
+ Faker::Internet.email)
+ end
+
+ extend Generator
+end
diff --git a/fake-service/lib/smail.rb b/fake-service/lib/smail.rb
new file mode 100644
index 00000000..6710f752
--- /dev/null
+++ b/fake-service/lib/smail.rb
@@ -0,0 +1,27 @@
+
+module Smail
+end
+
+require 'generator'
+require 'smail/security_casing'
+require 'smail/security_casing_examples'
+require 'smail/stats'
+require 'smail/stats_observer'
+require 'smail/sorted_mail'
+require 'smail/mail'
+require 'smail/persona'
+require 'smail/mail_service'
+require 'smail/fake'
+require 'smail/mailset'
+require 'smail/server'
+require 'smail/paginate'
+require 'smail/all'
+require 'smail/search'
+require 'smail/tags'
+require 'smail/combined_observer'
+require 'smail/contacts_observer'
+require 'smail/contact'
+require 'smail/contacts'
+require 'smail/contacts_sorter'
+require 'smail/contacts_search'
+require 'smail/mail_scope_filter'
diff --git a/fake-service/lib/smail/all.rb b/fake-service/lib/smail/all.rb
new file mode 100644
index 00000000..76da1f8b
--- /dev/null
+++ b/fake-service/lib/smail/all.rb
@@ -0,0 +1,8 @@
+
+module Smail
+ class All
+ def restrict(input)
+ input
+ end
+ end
+end
diff --git a/fake-service/lib/smail/combined_observer.rb b/fake-service/lib/smail/combined_observer.rb
new file mode 100644
index 00000000..a3e920ca
--- /dev/null
+++ b/fake-service/lib/smail/combined_observer.rb
@@ -0,0 +1,23 @@
+module Smail
+ class CombinedObserver
+ def initialize(*observers)
+ @observers = observers
+ end
+
+ def <<(observer)
+ @observers << observer
+ end
+
+ def mail_added(mail)
+ @observers.each { |o| o.mail_added(mail) }
+ end
+
+ def mail_removed(mail)
+ @observers.each { |o| o.mail_removed(mail) }
+ end
+
+ def mail_updated(before, after)
+ @observers.each { |o| o.mail_updated(before, after) }
+ end
+ end
+end
diff --git a/fake-service/lib/smail/contact.rb b/fake-service/lib/smail/contact.rb
new file mode 100644
index 00000000..a07f0f84
--- /dev/null
+++ b/fake-service/lib/smail/contact.rb
@@ -0,0 +1,26 @@
+
+module Smail
+ class Contact < Struct.new(:ident, :name, :addresses, :mails_received, :mails_sent, :last_received, :last_sent, :prev, :next)
+ include Comparable
+
+ def to_json(*args)
+ {
+ ident: self.ident,
+ name: self.name,
+ addresses: Array(self.addresses),
+ mails_received: self.mails_received || 0,
+ mails_sent: self.mails_sent || 0,
+ last_received: self.last_received,
+ last_sent: self.last_sent
+ }.to_json(*args)
+ end
+
+ def comparison_value
+ [(self.mails_received || 0) + (self.mails_sent || 0) * 0.8, self.last_received, self.last_sent]
+ end
+
+ def <=>(other)
+ other.comparison_value <=> self.comparison_value
+ end
+ end
+end
diff --git a/fake-service/lib/smail/contacts.rb b/fake-service/lib/smail/contacts.rb
new file mode 100644
index 00000000..af523a69
--- /dev/null
+++ b/fake-service/lib/smail/contacts.rb
@@ -0,0 +1,84 @@
+require 'mail'
+require 'set'
+
+module Smail
+ class Contacts
+ include Enumerable
+
+ def initialize(persona)
+ @persona = persona
+ @contacts = nil
+ @contacts_cache = {}
+ @contacts_lookup = {}
+ end
+
+ def contact(ix)
+ @contacts_lookup[ix]
+ end
+
+ def each
+ curr = @contacts
+ while curr
+ yield curr
+ curr = curr.next
+ end
+ end
+
+ def normalize(addr)
+ addr.downcase
+ end
+
+ def parse(a)
+ ::Mail::Address.new(a)
+ end
+
+ def update c, addr
+ @contacts_cache[normalize(addr.address)] = c
+ c.name = addr.display_name if addr.display_name
+ (c.addresses ||= Set.new) << addr.address
+ end
+
+ def create_new_contact(addr)
+ old_first = @contacts
+ c = Contact.new
+ c.ident = addr.hash.abs.to_s
+ c.next = old_first
+ @contacts_lookup[c.ident] = c
+ @contacts = c
+ update c, addr
+ c
+ end
+
+ def find_or_create(addr)
+ parsed = parse(addr)
+ if cc = @contacts_cache[normalize(parsed.address)]
+ update cc, parsed
+ cc
+ else
+ create_new_contact(parsed)
+ end
+ end
+
+ def latest(prev, n)
+ if prev && prev > n
+ prev
+ else
+ n
+ end
+ end
+
+ def new_mail_from(a, t)
+ contact = find_or_create(a)
+ contact.last_received = latest(contact.last_received, t)
+ contact.mails_received ||= 0
+ contact.mails_received += 1
+ end
+
+ def new_mail_to(a, t)
+ contact = find_or_create(a)
+ contact.last_sent = latest(contact.last_sent, t)
+ contact.mails_sent ||= 0
+ contact.mails_sent += 1
+ end
+ end
+end
diff --git a/fake-service/lib/smail/contacts_observer.rb b/fake-service/lib/smail/contacts_observer.rb
new file mode 100644
index 00000000..04c1670b
--- /dev/null
+++ b/fake-service/lib/smail/contacts_observer.rb
@@ -0,0 +1,54 @@
+module Smail
+ class ContactsObserver
+ def initialize(contacts)
+ @contacts = contacts
+ end
+
+ def extract_addresses(*addrs)
+ addrs.flatten.compact
+ end
+
+ def all_receivers(mail, &block)
+ extract_addresses(mail.to, mail.cc, mail.bcc).each(&block)
+ end
+
+ def all_senders(mail, &block)
+ extract_addresses(mail.from).each(&block)
+ end
+
+ def new_receivers(before, after, &block)
+ (extract_addresses(after.to, after.cc, after.bcc) - extract_addresses(before.to, before.cc, before.bcc)).each(&block)
+ end
+
+ def new_senders(before, after, &block)
+ (extract_addresses(after.from) - extract_addresses(before.from)).each(&block)
+ end
+
+ def timestamp_from(mail)
+ mail.headers[:date]
+ end
+
+ def mail_added(mail)
+ timestamp = timestamp_from(mail)
+ all_receivers(mail) do |rcv|
+ @contacts.new_mail_to(rcv, timestamp)
+ end
+ all_senders(mail) do |s|
+ @contacts.new_mail_from(s, timestamp)
+ end
+ end
+
+ def mail_removed(mail)
+ end
+
+ def mail_updated(before, after)
+ timestamp = timestamp_from(after)
+ new_receivers(before, after) do |rcv|
+ @contacts.new_mail_to(rcv, timestamp)
+ end
+ new_senders(before, after) do |s|
+ @contacts.new_mail_from(s, timestamp)
+ end
+ end
+ end
+end
diff --git a/fake-service/lib/smail/contacts_search.rb b/fake-service/lib/smail/contacts_search.rb
new file mode 100644
index 00000000..76ee6aa2
--- /dev/null
+++ b/fake-service/lib/smail/contacts_search.rb
@@ -0,0 +1,71 @@
+
+# Very simple search for contacts. The search string will be something that will be prefix matched
+# using a boundary before but not after. If you put in more than one word, those two will be searched
+# and ANDed together. You can use double quotes or single quotes to do the obvious thing instead
+module Smail
+ class ContactsSearch
+ def initialize(q)
+ @qtree = ContactsSearch.compile(q)
+ end
+
+ def restrict(input)
+ input.select do |mm|
+ @qtree.match?(mm)
+ end
+ end
+
+ REGEXP_DQUOTED = /"[^"]*"/
+ REGEXP_SQUOTED = /'[^']*'/
+ REGEXP_OTHER = /[^\s]+/
+
+ class AndMatch
+ attr_reader :data
+ def initialize(data = [])
+ @data = data
+ end
+ def <<(node)
+ @data << node
+ end
+ def match?(c)
+ self.data.all? { |mm| mm.match?(c) }
+ end
+ end
+
+ class StringMatch
+ def initialize(data, quoted=false)
+ @data = Regexp.new(Regexp.quote(if quoted
+ data[1..-2]
+ else
+ data
+ end), Regexp::IGNORECASE)
+ @exact_match = /\b#{@data}/
+ end
+
+ def match_string?(str)
+ Array(str).any? { |ff| @exact_match.match ff }
+ end
+
+ def match?(c)
+ match_string? ([c.name] + c.addresses.to_a).compact
+ end
+ end
+
+ def self.compile(q)
+ qs = StringScanner.new(q)
+ qtree = AndMatch.new
+ until qs.eos?
+ res =
+ if qs.check(REGEXP_DQUOTED)
+ StringMatch.new(qs.scan(REGEXP_DQUOTED), true)
+ elsif qs.check(REGEXP_SQUOTED)
+ StringMatch.new(qs.scan(REGEXP_SQUOTED), true)
+ elsif qs.check(REGEXP_OTHER)
+ StringMatch.new(qs.scan(REGEXP_OTHER))
+ end
+ qtree << res
+ qs.scan(/\s+/)
+ end
+ qtree
+ end
+ end
+end
diff --git a/fake-service/lib/smail/contacts_sorter.rb b/fake-service/lib/smail/contacts_sorter.rb
new file mode 100644
index 00000000..ca78177f
--- /dev/null
+++ b/fake-service/lib/smail/contacts_sorter.rb
@@ -0,0 +1,8 @@
+
+module Smail
+ class ContactsSorter
+ def restrict(input)
+ input.sort
+ end
+ end
+end
diff --git a/fake-service/lib/smail/fake.rb b/fake-service/lib/smail/fake.rb
new file mode 100644
index 00000000..b1b468ac
--- /dev/null
+++ b/fake-service/lib/smail/fake.rb
@@ -0,0 +1,146 @@
+module Smail
+ class << self
+ def mail_service
+ @mail_service ||= MailService.new
+ end
+ end
+
+ module Fake
+ PERSONAS = [
+ Persona.new(1, "Yago Macedo", nil, "sirineu@souza.org")
+ ]
+
+ def personas
+ PERSONAS.map(&:ident)
+ end
+
+ def persona(i)
+ PERSONAS.select { |x| x.ident.to_s == i}.first
+ end
+
+ def mails(query, page_number, window_size)
+ with_timing do
+ stats, mails = Smail.mail_service.mails(query, page_number, window_size)
+ { stats: stats, mails: mails.to_a }
+ end
+ end
+
+ def contacts(query, page_number, window_size)
+ with_timing do
+ contacts = Smail.mail_service.contacts(query, page_number, window_size)
+ { contacts: contacts.to_a }
+ end
+ end
+
+ def contact(ix)
+ Smail.mail_service.contact(ix)
+ end
+
+ def delete_mails(query, page_number, window_size, mails_idents)
+ idents = mails_idents.gsub(/[\[\]]/, '').split(',').collect {|x| x.to_i}
+ Smail.mail_service.delete_mails(query, page_number, window_size, idents)
+ []
+ end
+
+ def mail(i)
+ Smail.mail_service.mail(i)
+ end
+
+ def send_mail(data)
+ Smail.mail_service.send_mail(data)
+ end
+
+ def update_mail(data)
+ Smail.mail_service.update_mail(data)
+ end
+
+ def delete_mail(i)
+ Smail.mail_service.delete_mail(i)
+ end
+
+ def draft_reply_for(i)
+ Smail.mail_service.draft_reply_for(i)
+ end
+
+ def tags(i)
+ Smail.mail_service.mail(i).tag_names
+ end
+
+ def create_tag(tag_json)
+ Smail.mail_service.create_tag tag_json
+ end
+
+ def all_tags(q)
+ Smail.mail_service.tags(q)
+ end
+
+ def settags(i, body)
+ m = Smail.mail_service.mail(i)
+ m.tag_names = body["newtags"]
+ m.tag_names
+ end
+
+ def starmail(i, val)
+ m = Smail.mail_service.mail(i)
+ m.starred = val if m
+ ""
+ end
+
+ def repliedmail(i, val)
+ m = Smail.mail_service.mail(i)
+ m.replied = val if m
+ ""
+ end
+
+ def readmail(i, val)
+ m = Smail.mail_service.mail(i)
+ m.read = val if m
+ ""
+ end
+
+ def readmails(mail_idents, val)
+ idents = mail_idents.gsub(/[\[\]]/, '').split(',').collect {|x| x.to_i}
+ Smail.mail_service.each { |k,v| readmail(k.ident, val) if idents.include?(k.ident) }
+ []
+ end
+
+ def control_create_mail
+ Smail.mail_service.create
+ ""
+ end
+
+ def control_delete_mails
+ Smail.mail_service.clean
+ ""
+ end
+
+ def control_mailset_load(name)
+ with_timing do
+ {
+ stats: Smail.mail_service.load_mailset(name),
+ loaded: name
+ }
+ end
+ end
+
+ def stats
+ Smail.mail_service.stats_report
+ end
+
+ def with_timing
+ before = Time.now
+ result = yield
+ after = Time.now
+ res = case result
+ when Hash
+ result.dup
+ when nil
+ {}
+ else
+ { result: result }
+ end
+ res[:timing] = { duration: after - before }
+ res
+ end
+ end
+end
diff --git a/fake-service/lib/smail/mail.rb b/fake-service/lib/smail/mail.rb
new file mode 100644
index 00000000..8d1c8806
--- /dev/null
+++ b/fake-service/lib/smail/mail.rb
@@ -0,0 +1,299 @@
+module Smail
+ class Mail
+ attr_reader :to, :cc, :bcc, :from, :subject, :body, :headers, :status, :draft_reply_for
+ attr_accessor :ident, :security_casing
+
+ def initialize(data = {})
+ @ident = data[:ident]
+ @to = data[:to]
+ @cc = data[:cc]
+ @bcc = data[:bcc]
+ @from = data[:from]
+ @subject = data[:subject]
+ @body = data[:body]
+ @headers = data[:headers] || {}
+ @status = data[:status] || []
+ @draft_reply_for = data[:draft_reply_for] || []
+ self.tags = data[:tags] || Tags.new
+ end
+
+ def hash
+ @ident.hash
+ end
+
+ def eql?(object)
+ self == object
+ end
+
+ def tags=(t)
+ @tags.mail = nil if @tags
+ @tags = t
+ @tags.mail = self if @tags
+ t
+ end
+
+ def ==(object)
+ @ident == object.ident
+ end
+
+ def tag_names
+ @tags.names
+ end
+
+ def is_tagged?(t)
+ @tags.is_tagged?(t)
+ end
+
+ def tag_names=(vs)
+ to_remove = self.tag_names - vs
+ to_add = vs - self.tag_names
+
+ to_remove.each do |tn|
+ self.remove_tag(tn)
+ end
+
+ to_add.each do |v|
+ self.add_tag(v)
+ end
+ end
+
+ def add_tag(nm)
+ @tags.add_tag(nm)
+ end
+
+ def remove_tag(nm)
+ @tags.remove(nm)
+ end
+
+ def has_trash_tag?
+ tag_names.include? "trash"
+ end
+
+ def starred=(v); v ? add_status(:starred) : remove_status(:starred); end
+ def starred?; status?(:starred); end
+ def read=(v); v ? add_status(:read) : remove_status(:read); end
+ def read?; status?(:read); end
+ def replied=(v); v ? add_status(:replied) : remove_status(:replied); end
+ def replied?; status?(:replied); end
+
+ def add_status(n)
+ unless self.status?(n)
+ @status = @status + [n]
+ @tags.added_status(n)
+ Smail.mail_service.stats_status_added(n, self)
+ end
+ end
+
+ def remove_status(n)
+ if self.status?(n)
+ @status = @status - [n]
+ @tags.removed_status(n)
+ Smail.mail_service.stats_status_removed(n, self)
+ end
+ end
+
+ def status?(n)
+ @status.include?(n)
+ end
+
+ def to_json(*args)
+ {
+ header: {
+ to: Array(@to),
+ from: @from,
+ subject: @subject,
+ }.merge(@headers).merge({date: @headers[:date].iso8601}),
+ ident: ident,
+ tags: @tags.names,
+ status: @status,
+ security_casing: @security_casing,
+ draft_reply_for: @draft_reply_for,
+ body: @body
+ }.to_json(*args)
+ end
+
+
+ def self.from_json(obj, new_ident = nil)
+ ident = obj['ident']
+ draft_reply_for = obj['draft_reply_for']
+ hdrs = obj['header']
+ to = hdrs['to']
+ cc = hdrs['cc']
+ bcc = hdrs['bcc']
+ from = hdrs['from']
+ subject = hdrs['subject']
+ new_hdrs = {}
+ hdrs.each do |k,v|
+ new_hdrs[k.to_sym] = v unless %w(from to subject).include?(k)
+ end
+ tag_names = obj['tags']
+ st = obj['status']
+ bd = obj['body']
+
+ mail = new(:subject => subject,
+ :from => from,
+ :to => Array(to),
+ :cc => Array(cc),
+ :bcc => Array(bcc),
+ :headers => new_hdrs,
+ :status => st,
+ :draft_reply_for => draft_reply_for,
+ :ident => (ident.to_s.empty? ? new_ident : ident),
+ :body => bd)
+
+ tag_names.each do |tag_name|
+ mail.add_tag tag_name
+ end
+
+ mail
+
+ end
+
+ def to_s
+ ([
+ ("#{INTERNAL_TO_EXTERNAL_HEADER[:to]}: #{format_header_value_out(:to, @to)}" if @to),
+ ("#{INTERNAL_TO_EXTERNAL_HEADER[:from]}: #{format_header_value_out(:from, @from)}" if @from),
+ ("#{INTERNAL_TO_EXTERNAL_HEADER[:subject]}: #{format_header_value_out(:subject, @subject)}" if @subject),
+ ("#{INTERNAL_TO_EXTERNAL_HEADER[:x_tw_smail_tags]}: #{format_header_value_out(:x_tw_smail_tags, @tags.names)}" if !@tags.names.empty?),
+ ("#{INTERNAL_TO_EXTERNAL_HEADER[:x_tw_smail_status]}: #{format_header_value_out(:x_tw_smail_status, @status)}" if !@status.empty?),
+ ("#{INTERNAL_TO_EXTERNAL_HEADER[:x_tw_smail_ident]}: #{format_header_value_out(:x_tw_smail_ident, @ident)}"),
+ ].compact + @headers.map { |k,v| "#{INTERNAL_TO_EXTERNAL_HEADER[k]}: #{format_header_value_out(k, v)}"}).sort.join("\n") + "\n\n#{@body}"
+ end
+
+ SPECIAL_HEADERS = [:subject, :from, :to, :x_tw_smail_tags, :x_tw_smail_status, :x_tw_smail_ident]
+ INTERNAL_TO_EXTERNAL_HEADER = {
+ :subject => "Subject",
+ :date => "Date",
+ :from => "From",
+ :to => "To",
+ :cc => "CC",
+ :bcc => "BCC",
+ :message_id => "Message-ID",
+ :mime_version => "Mime-Version",
+ :content_type => "Content-Type",
+ :content_transfer_encoding => "Content-Transfer-Encoding",
+ :x_tw_smail_tags => "X-TW-SMail-Tags",
+ :x_tw_smail_status => "X-TW-SMail-Status",
+ :x_tw_smail_ident => "X-TW-SMail-Ident",
+ }
+
+ def format_header_value_out(k,v)
+ case k
+ when :date
+ v.strftime("%a, %d %b %Y %H:%M:%S %z")
+ when :to, :cc, :bcc
+ Array(v).join(", ")
+ when :x_tw_smail_tags, :x_tw_smail_status
+ v.join(", ")
+ else
+ v
+ end
+ end
+
+ class << self
+ def formatted_header(k, ls)
+ format_header_value(k, ls[k] && ls[k][1])
+ end
+
+ def has_header(hdr_name, ls, val, otherwise)
+ if ls[hdr_name]
+ val
+ else
+ otherwise
+ end
+ end
+
+ def time_rand from = (Time.now - 300000000), to = Time.now
+ Time.at(from + rand * (to.to_f - from.to_f))
+ end
+
+ # io is String or IO
+ def read(io, ident = nil)
+ io = StringIO.new(io) if io.is_a? String
+ headers = {}
+ body = ""
+ reading_headers = true
+ previous_header = nil
+ first = true
+ io.each do |ln|
+ if first && ln =~ /^From /
+ # Ignore line delimiter things
+ else
+ if reading_headers
+ if ln.chomp == ""
+ reading_headers = false
+ else
+ if previous_header && ln =~ /^\s+/
+ previous_header[1] << " #{ln.strip}"
+ else
+ key, value = ln.chomp.split(/: /, 2)
+ previous_header = [key, value]
+ headers[internal_header_name(key)] = previous_header
+ end
+ end
+ else
+ body << ln
+ end
+ end
+ if first
+ first = false
+ end
+ end
+
+ header_data = {}
+ headers.each do |k, (_, v)|
+ unless special_header?(k)
+ header_data[k] = format_header_value(k, v)
+ end
+ end
+
+ unless header_data[:date]
+ header_data[:date] = time_rand
+ end
+
+
+ new(:subject => formatted_header(:subject, headers),
+ :from => formatted_header(:from, headers),
+ :to => formatted_header(:to, headers),
+ :headers => header_data,
+ :tags => formatted_header(:x_tw_smail_tags, headers),
+ :status => formatted_header(:x_tw_smail_status, headers),
+ :ident => has_header(:x_tw_smail_ident, headers, formatted_header(:x_tw_smail_ident, headers), ident),
+ :body => body
+ )
+ end
+
+
+ private
+ def internal_header_name(k)
+ k.downcase.gsub(/-/, '_').to_sym
+ end
+
+ def special_header?(k)
+ SPECIAL_HEADERS.include?(k)
+ end
+
+ def format_header_value(k, v)
+ case k
+ when :date
+ DateTime.parse(v)
+ when :to, :cc, :bcc
+ vs = (v || "").split(/, /)
+ if vs.length == 1
+ vs[0]
+ else
+ vs
+ end
+ when :x_tw_smail_tags
+ Tags.new *(v || "").split(/, /)
+ when :x_tw_smail_status
+ (v || "").split(/, /).map { |ss| ss.to_sym }
+ when :x_tw_smail_ident
+ v.to_i
+ else
+ v
+ end
+ end
+ end
+ end
+end
diff --git a/fake-service/lib/smail/mail_scope_filter.rb b/fake-service/lib/smail/mail_scope_filter.rb
new file mode 100644
index 00000000..c8a5e042
--- /dev/null
+++ b/fake-service/lib/smail/mail_scope_filter.rb
@@ -0,0 +1,70 @@
+module Smail
+ module MailScopeFilter
+ include Enumerable
+
+ def initialize(c)
+ @c = c
+ end
+
+ def each
+ @c.each do |m|
+ yield m if retain?(m)
+ end
+ end
+
+ class Default
+ include MailScopeFilter
+
+ def initialize(c)
+ super
+ @tags = [Tags.get('sent'), Tags.get('trash'), Tags.get('drafts')]
+ end
+
+ def retain?(m)
+ !(@tags.any? { |t| m.is_tagged?(t) })
+ end
+
+ class << self
+ def +(o)
+ o
+ end
+ end
+ end
+
+ class All
+ include MailScopeFilter
+
+ def initialize(c)
+ super
+ @t = Tags.get('trash')
+ end
+
+ def retain?(m)
+ !m.is_tagged?(@t)
+ end
+
+ class << self
+ def +(o)
+ All
+ end
+ end
+ end
+
+ def self.tagged_with(n)
+ t = Tags.get(n)
+ c = Class.new
+ c.send :include, MailScopeFilter
+ c.send :define_method, :retain? do |m|
+ m.is_tagged?(t)
+ end
+ c.class.send :define_method, :+ do |o|
+ All === o ? All : self
+ end
+ c
+ end
+
+ Trash = tagged_with('trash')
+ Sent = tagged_with('sent')
+ Drafts = tagged_with('drafts')
+ end
+end
diff --git a/fake-service/lib/smail/mail_service.rb b/fake-service/lib/smail/mail_service.rb
new file mode 100644
index 00000000..314feaf5
--- /dev/null
+++ b/fake-service/lib/smail/mail_service.rb
@@ -0,0 +1,152 @@
+require 'set'
+
+module Smail
+ class MailService
+ include Enumerable
+ include Smail::Stats
+
+ def each
+ @mails.each do |mo|
+ yield mo
+ end
+ end
+
+ def initialize
+ self.clean
+ end
+
+ def contact(ix)
+ @contacts.contact(ix)
+ end
+
+ def contacts
+ @contacts.to_a
+ end
+
+ def clean
+ Smail::Tags.clean
+ @next_ident = 0
+ @reply_drafts = {}
+ @mails = SortedMail.new
+ @contacts = Contacts.new(Fake::PERSONAS[0])
+ @observers = CombinedObserver.new(StatsObserver.new(self),
+ ContactsObserver.new(@contacts))
+ end
+
+ def create(mail=Generator.random_mail)
+ unless mail.ident
+ mail.ident = @next_ident
+ @next_ident += 1
+ end
+ @mails[mail.ident] = mail
+ @observers.mail_added(mail)
+ end
+
+ def create_tag(tag_json)
+ Smail::Tags.create_tag tag_json['tag']
+ end
+
+ def mail(ix)
+ @mails[ix.to_i]
+ end
+
+ def send_mail(data)
+ ms = Mail.from_json(data, @next_ident)
+ if ms.tag_names.include?("sent")
+ ms.remove_tag "drafts"
+ @reply_drafts.delete ms.draft_reply_for
+ elsif ms.tag_names.include?("drafts") and ms.draft_reply_for
+ @reply_drafts[ms.draft_reply_for] = ms.ident
+ end
+ @next_ident += 1
+ @mails[ms.ident] = ms
+ update_status ms
+ @observers.mail_added(ms)
+ ms.ident
+ end
+
+ def draft_reply_for(ident)
+ @mails[@reply_drafts[ident.to_i]]
+ end
+
+ def update_mail(data)
+ mail = Mail.from_json(data)
+ before = @mails[mail.ident]
+ @mails[mail.ident] = mail
+ update_status mail
+ @observers.mail_updated(before, mail)
+ before.tags = nil
+ mail.ident
+ end
+
+ def update_status(mail)
+ mail.read = true
+ mail.headers[:date] = Time.now
+ end
+
+ def tags(q)
+ if q && !q.strip.empty?
+ query = /\b#{Regexp.new(Regexp.quote(q), Regexp::IGNORECASE)}/
+ Smail::Tags.all_tags.select do |tt|
+ query =~ tt.name
+ end
+ else
+ Smail::Tags.all_tags
+ end
+ end
+
+ def stats_report
+ { stats: self.stats }
+ end
+
+ def delete_mail(ix)
+ ms = @mails[ix.to_i]
+ @reply_drafts.delete ms.draft_reply_for
+
+ if ms.has_trash_tag?
+ m = @mails.delete ix.to_i
+ @observers.mail_removed(m)
+ m.tags = nil
+ else
+ ms.add_tag 'trash'
+ end
+ end
+
+ def load_mailset(name)
+ self.clean
+ ms = Smail::Mailset.load(name, @observers)
+ raise "couldn't find mailset #{name}" unless ms
+ @mails.add_all ms.mails
+ self.stats
+ end
+
+ def mails(q, page, window_size)
+ restrictors = [All.new]
+ restrictors << Paginate.new(page, window_size) if window_size > 0
+ restrictors << Search.new(q)
+ with_stats(restrictors.reverse.inject(self) do |ms, restr|
+ restr.restrict(ms)
+ end)
+ end
+
+ def contacts(q, page, window_size)
+ restrictors = [All.new]
+ restrictors << ContactsSorter.new
+ restrictors << Paginate.new(page, window_size) if window_size > 0
+ restrictors << ContactsSearch.new(q) if q
+ restrictors.reverse.inject(@contacts) do |c, restr|
+ restr.restrict(c)
+ end
+ end
+
+ def delete_mails(q, page, window_size, idents=nil)
+ unless idents.nil?
+ @mails.each { |k,v| delete_mail(k.ident) if idents.include?(k.ident) }
+ else
+ mails(q, page, window_size).each do |m|
+ delete_mail m.ident
+ end
+ end
+ end
+ end
+end
diff --git a/fake-service/lib/smail/mailset.rb b/fake-service/lib/smail/mailset.rb
new file mode 100644
index 00000000..0626dad8
--- /dev/null
+++ b/fake-service/lib/smail/mailset.rb
@@ -0,0 +1,84 @@
+require 'set'
+
+module Smail
+ class Mailset
+ DIR = File.expand_path File.join(File.dirname(__FILE__), "..", "..", "data", "mail-sets")
+
+ class << self
+ def create(name, number, tagging)
+ ms = new name, number, tagging, nil
+ ms.generate!
+ ms
+ end
+
+ def load(name, observers)
+ ms = new(name, -1, nil, observers)
+ if ms.load!
+ ms
+ else
+ nil
+ end
+ end
+ end
+
+ attr_reader :mails
+ attr_reader :tags
+
+ def initialize(name, number, tagging, observers)
+ @name, @number, @tagging, @observers = name, number, tagging, observers
+ end
+
+ def generate!
+ @persona = Generator.random_persona
+ @tags = Generator.tags(Generator.ladder_distribution(4, 40))
+
+ @mails = {}
+ @tags = Set.new
+ (0...(@number)).each do |i|
+ res = if @tagging
+ Generator.random_tagged_mail(@tags)
+ else
+ Generator.random_mail
+ end
+ @observers.mail_added res
+ @tags.merge res.tags
+ res.ident = i
+ @mails[res.ident] = res
+ end
+ end
+
+ def save!
+ dir = File.join(DIR, @name)
+ Dir.mkdir(dir) unless Dir.exists?(dir)
+ File.open(File.join(dir, "persona.yml"), "w") do |f|
+ f.write @persona.to_yaml
+ end
+
+ @mails.each do |(k, m)|
+ nm = "mbox%08x" % m.ident
+ File.open(File.join(dir, nm), "w") do |f|
+ f.write m.to_s
+ end
+ end
+ end
+
+ def load!
+ dir = File.join(DIR, @name)
+ return false unless Dir.exists?(dir)
+ @persona = YAML.load_file(File.join(dir, "persona.yml"))
+ @mails = {}
+ @ix = 0
+ Dir["#{dir}/mbox*"].each do |f|
+ File.open(f) do |fio|
+ res = Smail::Mail.read fio, @ix
+ res.read = true if (res.tag_names.include?('sent') || res.tag_names.include?('drafts'))
+ @mails[res.ident] = res
+ @observers.mail_added res
+ res.security_casing = SecurityCasingExamples::Case.case_from(res.ident.to_i)
+ @ix += 1
+ end
+ end
+ true
+ end
+ end
+end
diff --git a/fake-service/lib/smail/paginate.rb b/fake-service/lib/smail/paginate.rb
new file mode 100644
index 00000000..85d09196
--- /dev/null
+++ b/fake-service/lib/smail/paginate.rb
@@ -0,0 +1,15 @@
+
+module Smail
+ class Paginate
+ def initialize(page, window_size)
+ @start = page * window_size
+ @end = (page + 1) * window_size
+ end
+
+ def restrict(input)
+ PaginatedEnumerable.new(input, @start, @end)
+ end
+ end
+end
+
+require 'smail/paginated_enumerable'
diff --git a/fake-service/lib/smail/paginated_enumerable.rb b/fake-service/lib/smail/paginated_enumerable.rb
new file mode 100644
index 00000000..41c7f7bd
--- /dev/null
+++ b/fake-service/lib/smail/paginated_enumerable.rb
@@ -0,0 +1,29 @@
+
+module Smail
+ class Paginate
+ class PaginatedEnumerable
+ include Enumerable
+ def initialize(input, start, e)
+ @input = input
+ @start = start
+ @end = e
+ end
+
+ def each
+ @input.each_with_index do |v, ix|
+ if ix >= @end
+ return #we are done
+ elsif ix >= @start
+ yield v
+ end
+ end
+ end
+
+ def each_total
+ @input.each do |v|
+ yield v
+ end
+ end
+ end
+ end
+end
diff --git a/fake-service/lib/smail/persona.rb b/fake-service/lib/smail/persona.rb
new file mode 100644
index 00000000..47fe42dd
--- /dev/null
+++ b/fake-service/lib/smail/persona.rb
@@ -0,0 +1,12 @@
+module Smail
+ class Persona < Struct.new :ident, :name, :signature, :address
+ def to_json
+ {
+ ident: self.ident,
+ name: self.name,
+ signature: self.signature,
+ address: self.address
+ }.to_json
+ end
+ end
+end
diff --git a/fake-service/lib/smail/search.rb b/fake-service/lib/smail/search.rb
new file mode 100644
index 00000000..6c6b109e
--- /dev/null
+++ b/fake-service/lib/smail/search.rb
@@ -0,0 +1,133 @@
+# Syntax notes for search:
+# you can put a - in front of any search term to negate it
+# you can scope a search by putting a name of a scope, a colon and then the search term WITHOUT a space.
+# scoping will allow you to search for more things than otherwise available
+# an unknown scope name will be assumed to be a header to search
+# you can surround a search term in quotes to search for the whole thing
+# multiple search terms will be ANDed together
+# you can OR things by using the keyword OR/or - if you have it without parens, you will or the whole left with the whole right, until we find another or.
+# if you use parenthesis, you can group together terms
+# search in:_default_, in:all, in:trash, in:sent, in:drafts will only work for the WHOLE search. You can do a negation on a scoped search if it's in:trash, in:sent or in:drafts, but not for in:all
+
+module Smail
+ class Search
+ def initialize(q)
+ if q
+ @qtree, @search_scope = Search.compile(q)
+ else
+ @qtree, @search_scope = TrueMatch.new, Smail::MailScopeFilter::Default
+ end
+ end
+
+ def restrict(input)
+ @search_scope.new(input).select do |mm|
+ @qtree.match?(mm)
+ end
+ end
+
+ REGEXP_DQUOTED = /"[^"]*"/
+ REGEXP_SQUOTED = /'[^']*'/
+ REGEXP_SCOPE = /\w+:(".*?"|'.*?'|[^\s\)]+)/
+ REGEXP_OTHER = /[^\s\)]+/
+
+ def self.scan_literal(qs)
+ if qs.check(REGEXP_DQUOTED)
+ StringMatch.new(qs.scan(REGEXP_DQUOTED), true)
+ elsif qs.check(REGEXP_SQUOTED)
+ StringMatch.new(qs.scan(REGEXP_SQUOTED), true)
+ elsif qs.check(REGEXP_OTHER)
+ StringMatch.new(qs.scan(REGEXP_OTHER))
+ end
+ end
+
+ def self.combine_search_scopes(l, r)
+ l + r
+ end
+
+ def self.compile(q, qs = StringScanner.new(q))
+ qtree = AndMatch.new
+ search_scope = Smail::MailScopeFilter::Default
+ until qs.eos?
+ if qs.check(/\)/)
+ qs.scan(/\)/)
+ return optimized(qtree), search_scope
+ end
+
+ negated = false
+ if qs.check(/-/)
+ negated = true
+ qs.scan(/-/)
+ end
+
+ if qs.check(/or/i)
+ qs.scan(/or/i)
+ left = qtree
+ qtree = OrMatch.new(left, AndMatch.new)
+ else
+ res =
+ if qs.check(/\(/)
+ qs.scan(/\(/)
+ v, sc = compile(q, qs)
+ search_scope = search_scope + sc
+ v
+ elsif qs.check(REGEXP_DQUOTED)
+ StringMatch.new(qs.scan(REGEXP_DQUOTED), true)
+ elsif qs.check(REGEXP_SQUOTED)
+ StringMatch.new(qs.scan(REGEXP_SQUOTED), true)
+ elsif qs.check(REGEXP_SCOPE)
+ scope = qs.scan(/\w+/)
+ qs.scan(/:/)
+ rest_node = scan_literal(qs)
+ v = ScopeMatch.new(scope, rest_node)
+ if v.is_search_scope? && !negated
+ search_scope = search_scope + v.search_scope
+ TrueMatch.new
+ else
+ v
+ end
+ elsif qs.check(REGEXP_OTHER)
+ StringMatch.new(qs.scan(REGEXP_OTHER))
+ end
+ res = NegateMatch.new(res) if negated
+ qtree << res
+ end
+
+ qs.scan(/\s+/)
+ end
+ return optimized(qtree), search_scope
+ end
+
+ def self.optimized(tree)
+ case tree
+ when AndMatch
+ data = tree.data.reject { |d| TrueMatch === d }
+ if data.length == 1
+ optimized(data.first)
+ else
+ AndMatch.new(data.map { |n| optimized(n)} )
+ end
+ when OrMatch
+ if tree.right.is_a?(AndMatch) && tree.right.data.empty?
+ optimized(tree.left)
+ else
+ OrMatch.new(optimized(tree.left), optimized(tree.right))
+ end
+ when NegateMatch
+ if tree.data.is_a?(NegateMatch)
+ optimized(tree.data.data)
+ else
+ NegateMatch.new(optimized(tree.data))
+ end
+ else
+ tree
+ end
+ end
+ end
+end
+
+require 'smail/search/string_match'
+require 'smail/search/scope_match'
+require 'smail/search/negate_match'
+require 'smail/search/and_match'
+require 'smail/search/or_match'
+require 'smail/search/true_match'
diff --git a/fake-service/lib/smail/search/and_match.rb b/fake-service/lib/smail/search/and_match.rb
new file mode 100644
index 00000000..2bc53f0d
--- /dev/null
+++ b/fake-service/lib/smail/search/and_match.rb
@@ -0,0 +1,25 @@
+module Smail
+ class Search
+ class AndMatch
+ attr_reader :data
+ def initialize(data = [])
+ @data = data
+ end
+ def <<(node)
+ @data << node
+ end
+
+ def to_s
+ "And(#{@data.join(", ")})"
+ end
+
+ def match?(mail)
+ self.data.all? { |mm| mm.match?(mail) }
+ end
+
+ def match_string?(str)
+ self.data.all? { |mm| mm.match_string?(str) }
+ end
+ end
+ end
+end
diff --git a/fake-service/lib/smail/search/negate_match.rb b/fake-service/lib/smail/search/negate_match.rb
new file mode 100644
index 00000000..f8bb59d4
--- /dev/null
+++ b/fake-service/lib/smail/search/negate_match.rb
@@ -0,0 +1,22 @@
+module Smail
+ class Search
+ class NegateMatch
+ attr_reader :data
+ def initialize(data)
+ @data = data
+ end
+
+ def to_s
+ "Negate(#@data)"
+ end
+
+ def match?(mail)
+ !self.data.match?(mail)
+ end
+
+ def match_string?(str)
+ !self.data.match_string?(str)
+ end
+ end
+ end
+end
diff --git a/fake-service/lib/smail/search/or_match.rb b/fake-service/lib/smail/search/or_match.rb
new file mode 100644
index 00000000..455923bf
--- /dev/null
+++ b/fake-service/lib/smail/search/or_match.rb
@@ -0,0 +1,25 @@
+module Smail
+ class Search
+ class OrMatch
+ attr_reader :left, :right
+ def initialize(left, right)
+ @left = left
+ @right = right
+ end
+ def <<(node)
+ @right << node
+ end
+ def to_s
+ "Or(#@left, #@right)"
+ end
+
+ def match?(mail)
+ [@left, @right].any? { |mm| mm.match?(mail) }
+ end
+
+ def match_string?(str)
+ [@left, @right].any? { |mm| mm.match_string?(str) }
+ end
+ end
+ end
+end
diff --git a/fake-service/lib/smail/search/scope_match.rb b/fake-service/lib/smail/search/scope_match.rb
new file mode 100644
index 00000000..4402674d
--- /dev/null
+++ b/fake-service/lib/smail/search/scope_match.rb
@@ -0,0 +1,79 @@
+
+module Smail
+ class Search
+ class ScopeMatch
+ def initialize(scope, data)
+ @scope = scope.downcase.gsub(/-/, '_').to_sym
+ @data = data
+ end
+
+ def to_s
+ "Scope(#@scope, #@data)"
+ end
+
+ def is_search_scope?
+ [:in, :tag, :is].include?(@scope) &&
+ %w(_default_ trash all sent drafts).include?(@data.match_string.downcase)
+ end
+
+ def search_scope
+ case @data.match_string.downcase
+ when '_default_'
+ Smail::MailScopeFilter::Default
+ when 'all'
+ Smail::MailScopeFilter::All
+ when 'trash'
+ Smail::MailScopeFilter::Trash
+ when 'sent'
+ Smail::MailScopeFilter::Sent
+ when 'drafts'
+ Smail::MailScopeFilter::Drafts
+ end
+ end
+
+ def match?(mail)
+ strs =
+ case @scope
+ when :to
+ mail.to
+ when :from, :sender
+ mail.from
+ when :cc
+ mail.headers[:cc]
+ when :bcc
+ mail.headers[:bcc]
+ when :subject
+ mail.subject
+ when :rcpt, :rcpts, :recipient, :recipients
+ [mail.to, mail.headers[:cc], mail.headers[:bcc]].flatten.compact
+ when :body
+ mail.body
+ when :tag, :tags, :in
+ return @data.match_exact_string?(mail.tag_names)
+ # has:seal, has:imprint, has:lock
+ when :is
+ case @data.str
+ when "starred"
+ return mail.starred?
+ when "read"
+ return mail.read?
+ when "replied"
+ return mail.replied?
+ # sealed, imprinted, signed, locked, encrypted,
+ else
+ raise "NOT IMPLEMENTED: is:#{@data}"
+ end
+ when :before
+ raise "NOT IMPLEMENTED"
+ when :after
+ raise "NOT IMPLEMENTED"
+ when :att, :attachment
+ raise "NOT IMPLEMENTED"
+ else
+ mail.headers[@scope] || (return false)
+ end
+ @data.match_string? strs
+ end
+ end
+ end
+end
diff --git a/fake-service/lib/smail/search/string_match.rb b/fake-service/lib/smail/search/string_match.rb
new file mode 100644
index 00000000..fc17ab59
--- /dev/null
+++ b/fake-service/lib/smail/search/string_match.rb
@@ -0,0 +1,37 @@
+module Smail
+ class Search
+ class StringMatch
+ attr_reader :str
+ def initialize(data, quoted=false)
+ @str = data
+ @quoted = quoted
+ @data = Regexp.new(Regexp.quote(self.match_string), Regexp::IGNORECASE)
+ @exact_match = /^#{@data}$/
+ end
+
+ def match_string
+ if @quoted
+ @str[1..-2]
+ else
+ @str
+ end
+ end
+
+ def to_s
+ "String(#@data)"
+ end
+
+ def match_string?(str)
+ Array(str).any? { |ff| !!(ff[@data]) }
+ end
+
+ def match_exact_string?(str)
+ Array(str).any? { |ff| @exact_match.match ff }
+ end
+
+ def match?(mail)
+ match_string? [mail.to, mail.from, mail.subject, mail.body]
+ end
+ end
+ end
+end
diff --git a/fake-service/lib/smail/search/true_match.rb b/fake-service/lib/smail/search/true_match.rb
new file mode 100644
index 00000000..7ac14923
--- /dev/null
+++ b/fake-service/lib/smail/search/true_match.rb
@@ -0,0 +1,13 @@
+module Smail
+ class Search
+ class TrueMatch
+ def match?(mail)
+ true
+ end
+
+ def match_string?(str)
+ true
+ end
+ end
+ end
+end
diff --git a/fake-service/lib/smail/security_casing.rb b/fake-service/lib/smail/security_casing.rb
new file mode 100644
index 00000000..fe8ae42b
--- /dev/null
+++ b/fake-service/lib/smail/security_casing.rb
@@ -0,0 +1,55 @@
+
+module Smail
+ class SecurityCasing < Struct.new(:imprints, :locks)
+ def to_json(*args)
+ { imprints: self.imprints, locks: self.locks }.to_json(*args)
+ end
+
+ def +(other)
+ imprints = self.imprints + other.imprints
+ locks = self.locks + other.locks
+ SecurityCasing.new(imprints, locks)
+ end
+
+ class Key < Struct.new :longid, :fingerprint, :user_ids, :connected_contacts, :state, :size, :algorithm, :trust, :validity
+ VALID_STATES = [:valid, :expired, :revoked]
+ VALID_TRUST = [:unknown, :no_trust, :marginal, :full, :ultimate]
+
+ def to_json(*args)
+ { longid: self.longid,
+ fingerprint: self.fingerprint,
+ user_ids: self.user_ids,
+ connected_contacts: self.connected_contacts,
+ state: self.state,
+ size: self.size,
+ algorithm: self.algorithm,
+ trust: self.trust,
+ validity: self.validity }.to_json(*args)
+ end
+ end
+
+ # Signature
+ class Imprint < Struct.new :seal, :imprint_timestamp, :algorithm, :state
+ VALID_STATES = [:valid, :invalid, :no_match, :from_expired, :from_revoked]
+
+ def to_json(*args)
+ { seal: self.seal,
+ imprint_timestamp: self.imprint_timestamp,
+ algorithm: self.algorithm,
+ state: self.state }.to_json(*args)
+ end
+ end
+
+ # Encryption
+ class Lock < Struct.new :key, :state, :algorithm, :key_specified_in_lock
+ VALID_STATES = [:valid, :failure, :no_private_key]
+
+ def to_json(*args)
+ { state: self.state,
+ algorithm: self.algorithm,
+ key: self.key,
+ key_specified_in_lock: self.key_specified_in_lock }.to_json(*args)
+ end
+ end
+ end
+end
diff --git a/fake-service/lib/smail/security_casing_examples.rb b/fake-service/lib/smail/security_casing_examples.rb
new file mode 100644
index 00000000..91bf80bb
--- /dev/null
+++ b/fake-service/lib/smail/security_casing_examples.rb
@@ -0,0 +1,142 @@
+module Smail
+ module SecurityCasingExamples
+ module Key
+ VALID_U_U = SecurityCasing::Key.new("295C746984AF7F0C", "698E2885C1DE74E32CD503AD295C746984AF7F0C", ["Ola Bini <ola@bini.ec>",
+ "Ola Bini <ola@olabini.se>"], ["1905060826932239808", "3264050876889579764"], :valid, 4096, :RSA, :ultimate, :ultimate)
+
+ EXPIRED_UN_UN = SecurityCasing::Key.new("05A63421F637E333", "41EA1D94F26186026CD4B2B505A63421F637E333", ["Rylee Elise Fowler <rylee@rylee.me>",
+ "Rylee Fowler (gmail mail, generally unused) <gando.depth@gmail.com>"], [], :expired, 2048, :RSA, :unknown, :unknown)
+
+ REVOKED_F_UN = SecurityCasing::Key.new("11044FD19FC527CC", "E64F19EBBBE86AA97AF36FD511044FD19FC527CC", ["Michael Rogers <michael@briarproject.org>"], [], :revoked, 2048, :RSA, :full, :unknown)
+
+ EXPIRED_NO_TRUST = SecurityCasing::Key.new("FB3973E142A913A4", "AB465EE7022B68C42DBAD324FB3973E142A913A4", ["Michael Granger <ged@FaerieMUD.org>", "Michael Granger <mgranger@rmd.net>", "Michael Granger <mgranger@wwwa.com>",
+ "Michael Granger <rubymage@gmail.com>", "Ged the Grey's Hain <ged@FaerieMUD.org>",
+ "Michael Granger <devEiant@devEiate.org>", "Michael Granger <mgranger@800-all-news.com>",
+ "Michael Granger <mgranger@rubycrafters.com>", "Michael Granger (at work) <mgranger@laika.com>"],
+ [], :expired, 1024, :DSA, :no_trust, :no_trust)
+
+ VALID_NO_TRUST = SecurityCasing::Key.new("29F16F77D77A211F", "692B652B70BC67E8EEA36E0929F16F77D77A211F", ["Christian Trabold <info@christian-trabold.de>",
+ "Christian Trabold <christian.trabold@dkd.de>"], [], :valid, 4096, :RSA, :no_trust, :no_trust)
+
+ EXAMPLES = [
+ VALID_U_U,
+ SecurityCasing::Key.new("37561129CF4BE610", "D37F700C25569B6F1E1286EF37561129CF4BE610", ["Molly <mollyslop@37dias.com>"], [], :valid, 4096, :RSA, :ultimate, :no_trust),
+ EXPIRED_UN_UN,
+ EXPIRED_NO_TRUST,
+ SecurityCasing::Key.new("E62030AB4AA41495", "BEB9D9E74B0C5167C5FC6CC8E62030AB4AA41495", ["Christopher Dell <chris@tigrish.com>"], ["4117763089493997091"], :valid, 2048, :RSA, :unknown, :full),
+ SecurityCasing::Key.new("D692003DAA02C70A", "3E053E70DE40B13ADE913E7ED692003DAA02C70A", ["Tyler Hicks <tyhicks@tyhicks.net>", "Tyler Hicks <tyhicks@ou.edu>", "Tyler Hicks <tyhicks@gmail.com>",
+ "Tyler Hicks <tyhicks@kernel.org>", "Tyler Hicks <tyhicks@ubuntu.com>", "Tyler Hicks <tyhicks@canonical.com>",
+ "Tyler Hicks <tyler.hicks@ubuntu.com>", "Tyler Hicks <tyler.hicks@canonical.com>"],
+ ["792826280999687388"], :valid, 4096, :RSA, :marginal, :full),
+ REVOKED_F_UN,
+ VALID_NO_TRUST,
+ SecurityCasing::Key.new("5DFEA1062EA46E4F", "14D07803BFF6EFA099988C4B5DFEA1062EA46E4F", ["Henri Salo <henri@nerv.fi>", "Henri Salo <fgeek@kapsi.fi>", "Henri Salo <henri.salo@kapsi.fi>",
+ "Henri Salo <henri.salo@qentinel.com>"], [], :valid, 1024, :DSA, :no_trust, :ultimate),
+ SecurityCasing::Key.new("C44FBF8A41A80850", "EE6497E3FEC3773BAD33062DC44FBF8A41A80850", ["Seeta Gangadharan <gangadharan@opentechinstitute.org>"], [], :valid, 2048, :RSA, :marginal, :full),
+ SecurityCasing::Key.new("7934ED27275BDB05", "9A6D46E5E7489C60A1DEFDCA7934ED27275BDB05", ["Ernesto Medina Delgado <edelgado@thoughtworks.com>"], ["2618860400823413108"], :valid, 4096, :RSA, :full, :unknown),
+ SecurityCasing::Key.new("EC73D77206A7B07F", "728CFCA32AAFF261DE88971CEC73D77206A7B07F", ["Aaron Bedra <aaron@thinkrelevance.com>"], ["1494195372012211732"], :revoked, 1024, :DSA, :ultimate, :ultimate),
+ SecurityCasing::Key.new("ACD5E501207FBB0E", "281C60C3D20A19C7D2608302ACD5E501207FBB0E", ["Chip Collier <photex@gmail.com>"], [], :valid, 2048, :RSA, :full, :ultimate),
+ SecurityCasing::Key.new("2ECDE8FDF22DB236", "E6D4C474A259113A02E8F2772ECDE8FDF22DB236", ["Hiroshi Nakamura (NaHi) <nahi@ruby-lang.org>", "Hiroshi Nakamura (NaHi) <nahi@ctor.org>",
+ "Hiroshi Nakamura (NaHi) <nakahiro@gmail.com>"], [], :revoked, 2048, :RSA, :no_trust, :marginal),
+ SecurityCasing::Key.new("482ECB2BDAAC67D2", "A00620D62EA9B36A3BB71BDE482ECB2BDAAC67D2", ["severino <irregulator@riseup.net>"], ["2361077248315571916"], :valid, 4096, :RSA, :full, :no_trust),
+ ]
+
+ end
+
+ module Imprint
+ VALID = SecurityCasing::Imprint.new(Key::VALID_U_U, Time.now - 200_123, :SHA256, :valid)
+ INVALID = SecurityCasing::Imprint.new(Key::VALID_U_U, Time.now - 123_345, :SHA256, :invalid)
+ NO_MATCH = SecurityCasing::Imprint.new(nil, Time.now - 42_134, :SHA128, :no_match)
+ FROM_EXPIRED = SecurityCasing::Imprint.new(Key::EXPIRED_UN_UN, Time.now - 1_002_123, :SHA128, :from_expired)
+ FROM_REVOKED = SecurityCasing::Imprint.new(Key::REVOKED_F_UN, Time.now - 12_123, :SHA128, :from_revoked)
+ VALID_FROM_NO_TRUST = SecurityCasing::Imprint.new(Key::VALID_NO_TRUST, Time.now - 10_424, :SHA512, :valid)
+
+ EXAMPLES = [
+ VALID,
+ INVALID,
+ NO_MATCH,
+ FROM_EXPIRED,
+ FROM_REVOKED,
+ VALID_FROM_NO_TRUST
+ ]
+ end
+
+ module Lock
+ VALID_TO_SPECIFIC = SecurityCasing::Lock.new(Key::VALID_U_U, :valid, :RSA, true)
+ VALID_NOT_TO_SPECIFIC = SecurityCasing::Lock.new(Key::VALID_U_U, :valid, :RSA, false)
+
+ INVALID_TO_SPECIFIC = SecurityCasing::Lock.new(Key::VALID_U_U, :failure, :RSA, true)
+ INVALID_NOT_TO_SPECIFIC = SecurityCasing::Lock.new(Key::VALID_U_U, :failure, :RSA, false)
+
+ EXPIRED_TO_SPECIFIC = SecurityCasing::Lock.new(Key::EXPIRED_UN_UN, :failure, :RSA, true)
+ EXPIRED_NOT_TO_SPECIFIC = SecurityCasing::Lock.new(Key::EXPIRED_UN_UN, :failure, :RSA, false)
+
+ REVOKED_TO_SPECIFIC = SecurityCasing::Lock.new(Key::REVOKED_F_UN, :failure, :RSA, true)
+ REVOKED_NOT_TO_SPECIFIC = SecurityCasing::Lock.new(Key::REVOKED_F_UN, :failure, :RSA, false)
+
+ NO_KEY_TO_SPECIFIC = SecurityCasing::Lock.new(nil, :no_private_key, :RSA, true)
+ NO_KEY_NOT_TO_SPECIFIC = SecurityCasing::Lock.new(nil, :no_private_key, :RSA, false)
+
+ EXAMPLES = [
+ VALID_TO_SPECIFIC,
+ VALID_NOT_TO_SPECIFIC,
+ INVALID_TO_SPECIFIC,
+ INVALID_NOT_TO_SPECIFIC,
+ EXPIRED_TO_SPECIFIC,
+ EXPIRED_NOT_TO_SPECIFIC,
+ REVOKED_TO_SPECIFIC,
+ REVOKED_NOT_TO_SPECIFIC,
+ NO_KEY_TO_SPECIFIC,
+ NO_KEY_NOT_TO_SPECIFIC
+ ]
+ end
+
+ module Case
+ NO_IMPRINTS_OR_LOCKS = SecurityCasing.new([], [])
+
+ ONE_VALID_IMPRINT = SecurityCasing.new([Imprint::VALID], [])
+ THREE_VALID_IMPRINTS = SecurityCasing.new([Imprint::VALID,
+ Imprint::VALID,
+ Imprint::VALID], [])
+ ONE_VALID_TWO_NO_MATCH_IMPRINTS = SecurityCasing.new([Imprint::NO_MATCH,
+ Imprint::VALID,
+ Imprint::NO_MATCH], [])
+ ONE_INVALID_IMPRINT = SecurityCasing.new([Imprint::INVALID], [])
+ ONE_NO_MATCH_IMPRINT = SecurityCasing.new([Imprint::NO_MATCH], [])
+ FROM_EXPIRED_IMPRINT = SecurityCasing.new([Imprint::FROM_EXPIRED], [])
+ FROM_REVOKED_IMPRINT = SecurityCasing.new([Imprint::FROM_REVOKED], [])
+ FROM_VALID_WITH_NO_TRUST_IMPRINT = SecurityCasing.new([Imprint::VALID_FROM_NO_TRUST], [])
+
+ WITH_IMPRINTS = [ONE_VALID_IMPRINT, THREE_VALID_IMPRINTS, ONE_VALID_TWO_NO_MATCH_IMPRINTS, ONE_INVALID_IMPRINT, ONE_NO_MATCH_IMPRINT, FROM_EXPIRED_IMPRINT, FROM_REVOKED_IMPRINT, FROM_VALID_WITH_NO_TRUST_IMPRINT]
+
+ VALID_TO_SPECIFIC_LOCK = SecurityCasing.new([], [Lock::VALID_TO_SPECIFIC])
+ VALID_NOT_TO_SPECIFIC_LOCK = SecurityCasing.new([], [Lock::VALID_NOT_TO_SPECIFIC])
+ ONE_VALID_TWO_INVALID_LOCKS = SecurityCasing.new([], [Lock::VALID_TO_SPECIFIC, Lock::INVALID_TO_SPECIFIC, Lock::INVALID_NOT_TO_SPECIFIC])
+ ONE_VALID_TWO_NO_KEY_LOCKS = SecurityCasing.new([], [Lock::NO_KEY_TO_SPECIFIC, Lock::VALID_TO_SPECIFIC, Lock::NO_KEY_NOT_TO_SPECIFIC])
+ INVALID_TO_SPECIFIC_LOCK = SecurityCasing.new([], [Lock::INVALID_TO_SPECIFIC])
+ INVALID_NOT_TO_SPECIFIC_LOCK = SecurityCasing.new([], [Lock::INVALID_NOT_TO_SPECIFIC])
+ EXPIRED_TO_SPECIFIC_LOCK = SecurityCasing.new([], [Lock::EXPIRED_TO_SPECIFIC])
+ EXPIRED_NOT_TO_SPECIFIC_LOCK = SecurityCasing.new([], [Lock::EXPIRED_NOT_TO_SPECIFIC])
+ REVOKED_TO_SPECIFIC_LOCK = SecurityCasing.new([], [Lock::REVOKED_TO_SPECIFIC])
+ REVOKED_NOT_TO_SPECIFIC_LOCK = SecurityCasing.new([], [Lock::REVOKED_NOT_TO_SPECIFIC])
+ NO_KEY_TO_SPECIFIC_LOCK = SecurityCasing.new([], [Lock::NO_KEY_TO_SPECIFIC])
+ NO_KEY_NOT_TO_SPECIFIC_LOCK = SecurityCasing.new([], [Lock::NO_KEY_NOT_TO_SPECIFIC])
+
+ WITH_LOCKS = [VALID_TO_SPECIFIC_LOCK, VALID_NOT_TO_SPECIFIC_LOCK, ONE_VALID_TWO_INVALID_LOCKS, ONE_VALID_TWO_NO_KEY_LOCKS, INVALID_TO_SPECIFIC_LOCK, INVALID_NOT_TO_SPECIFIC_LOCK,
+ EXPIRED_TO_SPECIFIC_LOCK, EXPIRED_NOT_TO_SPECIFIC_LOCK, REVOKED_TO_SPECIFIC_LOCK, REVOKED_NOT_TO_SPECIFIC_LOCK, NO_KEY_TO_SPECIFIC_LOCK, NO_KEY_NOT_TO_SPECIFIC_LOCK]
+
+ WITH_IMPRINTS_AND_LOCKS = WITH_IMPRINTS.product(WITH_LOCKS).map { |l, r| l + r }
+
+ EXAMPLES = [NO_IMPRINTS_OR_LOCKS] +
+ WITH_IMPRINTS +
+ WITH_LOCKS +
+ WITH_IMPRINTS_AND_LOCKS
+
+ class << self
+ def case_from(ident)
+ EXAMPLES[ident % EXAMPLES.size]
+ end
+ end
+ end
+ end
+end
diff --git a/fake-service/lib/smail/server.rb b/fake-service/lib/smail/server.rb
new file mode 100644
index 00000000..a5db8e03
--- /dev/null
+++ b/fake-service/lib/smail/server.rb
@@ -0,0 +1,82 @@
+require 'sinatra/base'
+require 'sinatra/json'
+require 'json'
+require 'net/http'
+
+module Smail
+ class Server < Sinatra::Base
+ set :root, File.join(File.dirname(__FILE__), '../../')
+
+ def json_body; JSON.parse request.body.read.to_s; end
+
+ if ENV['RACK_ENV'] == 'staging'
+ get '/' do File.read(File.join(settings.root, 'public', 'index.html')) end
+ end
+
+ get '/mails' do json mails(params["q"], (params["p"] || 0).to_i, (params["w"] || -1).to_i) end
+ delete '/mails' do json delete_mails(params["q"], (params["p"] || 0).to_i, (params["w"] || -1).to_i, params["idents"]) end
+ post '/mails/read' do json readmails(params["idents"], true) end
+ post '/mails/unread' do json readmails(params["idents"], false) end
+ get '/mail/:ident' do |i| json mail(i) end
+ delete '/mail/:ident' do |i| json delete_mail(i) end
+ post '/mail/:ident/star' do |i| json starmail(i, true) end
+ post '/mail/:ident/unstar' do |i| json starmail(i, false) end
+ post '/mail/:ident/replied' do |i| json repliedmail(i, true) end
+ post '/mail/:ident/unreplied' do |i| json repliedmail(i, false) end
+ post '/mail/:ident/read' do |i| json readmail(i, true) end
+ post '/mail/:ident/unread' do |i| json readmail(i, false) end
+ get '/mail/:ident/tags' do |i| json tags(i) end
+ post '/mail/:ident/tags' do |i| json settags(i, json_body) end
+
+ get '/draft_reply_for/:ident' do |i| json draft_reply_for(i) end
+
+ get '/contacts' do json contacts(params["q"], (params["p"] || 0).to_i, (params["w"] || -1).to_i) end
+ get '/contact/:ident' do |i| json contact(i) end
+
+ get '/stats' do json stats end
+
+ get '/personas' do json personas end
+ get '/persona/:ident' do |i| json persona(i) end
+
+ get '/tags' do json all_tags(params["q"]) end
+
+ post '/mails' do
+ ident = send_mail json_body
+ json({ ident: ident })
+ end
+
+ put '/mails' do
+ ident = update_mail json_body
+ json({ ident: ident })
+ end
+
+ post '/tags' do
+ tag = create_tag json_body
+ json({ tag: tag })
+ end
+
+ post '/control/create_mail' do json control_create_mail end
+ post '/control/delete_mails' do json control_delete_mails end
+ post '/control/mailset/:name/load' do |name| json control_mailset_load(name) end
+
+
+ # pass all other requests to asset server
+ get '/*' do
+ url = "http://localhost:9000/#{params['splat'][0]}"
+
+ resp = Net::HTTP.get_response(URI.parse(url))
+ if resp.is_a?(Net::HTTPSuccess)
+ res = resp.body.to_s.gsub(/(href|src)=("|')\//, '\1=\2' + url + '/')
+ content_type resp.content_type
+ status resp.code
+ res
+ else
+ status resp.code
+ resp.message
+ end
+ end
+
+
+ include Smail::Fake
+ end
+end
diff --git a/fake-service/lib/smail/sorted_mail.rb b/fake-service/lib/smail/sorted_mail.rb
new file mode 100644
index 00000000..803a8348
--- /dev/null
+++ b/fake-service/lib/smail/sorted_mail.rb
@@ -0,0 +1,57 @@
+module Smail
+ class SortedMail
+ include Enumerable
+
+ NEWEST_FIRST = lambda do |l,r|
+ (r.headers[:date] || Time.now) <=> (l.headers[:date] || Time.now)
+ end
+
+ def initialize(&block)
+ @mails = {}
+ @mail_order = []
+ @sort_procedure = block || NEWEST_FIRST
+ end
+
+ def []=(k, v)
+ @mails[k] = v
+ @mail_order << v
+ sort_mail_order!
+ v
+ end
+
+ def [](k)
+ @mails[k]
+ end
+
+ def delete(k)
+ v = @mails.delete(k)
+ @mail_order.delete(v)
+ v
+ end
+
+ def add_all(hs)
+ hs.each do |h,v|
+ @mails[h] = v
+ @mail_order << v
+ end
+ sort_mail_order!
+ self
+ end
+
+ def length
+ @mails.length
+ end
+
+ def sort_mail_order!
+ @mail_order.sort!(&@sort_procedure)
+ @mail_order.compact!
+ @mail_order.uniq!
+ end
+
+ def each
+ @mail_order.each do |m|
+ yield m
+ end
+ end
+ end
+end
diff --git a/fake-service/lib/smail/stats.rb b/fake-service/lib/smail/stats.rb
new file mode 100644
index 00000000..4e0393a4
--- /dev/null
+++ b/fake-service/lib/smail/stats.rb
@@ -0,0 +1,60 @@
+
+module Smail
+ module Stats
+ class StatsCollector
+ include Stats
+ def initialize
+ stats_init
+ end
+ end
+
+ attr_reader :stats
+
+ def stats_init
+ @stats = {
+ total: 0,
+ read: 0,
+ starred: 0,
+ replied: 0
+ }
+ end
+
+ def stats_added(m)
+ @stats[:total] += 1
+ stats_status_added(:read, m) if m.status?(:read)
+ stats_status_added(:replied, m) if m.status?(:replied)
+ stats_status_added(:starred, m) if m.status?(:starred)
+ end
+
+ def stats_removed(m)
+ @stats[:total] -= 1
+ stats_status_removed(:read, m) if m.status?(:read)
+ stats_status_removed(:replied, m) if m.status?(:replied)
+ stats_status_removed(:starred, m) if m.status?(:starred)
+ end
+
+ def stats_status_added(s, m)
+ @stats[s] += 1
+ end
+
+ def stats_status_removed(s, m)
+ @stats[s] -= 1
+ end
+
+ def each_total_helper(enum)
+ if enum.respond_to?(:each_total)
+ enum.each_total { |x| yield x }
+ else
+ enum.each { |x| yield x }
+ end
+ end
+
+ def with_stats(enum)
+ sc = StatsCollector.new
+ each_total_helper(enum) do |e|
+ sc.stats_added(e)
+ end
+ [sc.stats, enum]
+ end
+ end
+end
diff --git a/fake-service/lib/smail/stats_observer.rb b/fake-service/lib/smail/stats_observer.rb
new file mode 100644
index 00000000..f4f9b1cd
--- /dev/null
+++ b/fake-service/lib/smail/stats_observer.rb
@@ -0,0 +1,19 @@
+module Smail
+ class StatsObserver
+ def initialize(stats)
+ @stats = stats
+ @stats.stats_init
+ end
+
+ def mail_added(mail)
+ @stats.stats_added mail
+ end
+
+ def mail_removed(mail)
+ @stats.stats_removed mail
+ end
+
+ def mail_updated(before, after)
+ end
+ end
+end
diff --git a/fake-service/lib/smail/tags.rb b/fake-service/lib/smail/tags.rb
new file mode 100644
index 00000000..0973fc3d
--- /dev/null
+++ b/fake-service/lib/smail/tags.rb
@@ -0,0 +1,150 @@
+module Smail
+ class Tag < Struct.new(:name, :total_count, :read, :starred, :replied, :default)
+ def to_json(*args)
+ {
+ name: self.name,
+ ident: self.name.hash.abs,
+ default: self.default,
+ counts: {
+ total: self.total_count,
+ read: self.read,
+ starred: self.starred,
+ replied: self.replied,
+ }
+ }.to_json(*args)
+ end
+ end
+
+ class Tags
+ SPECIAL = %w(inbox sent drafts trash)
+
+ def initialize(*desired_names)
+ @tags = desired_names.each_with_object({}) do |name, res|
+ res[normalized(name)] = Tags.get(name)
+ end
+ end
+
+ def increase_all_count(t = nil)
+ change_all_count 1, t, :total_count
+ end
+
+ def decrease_all_count(t = nil)
+ change_all_count -1, t, :total_count
+ end
+
+ def change_all_count(v, t, s)
+ tags = t ? [t] : @tags.values
+ tags.each do |tag|
+ tag[s] += v
+ end
+ end
+
+ def increase_status_count_checked(status, t = nil)
+ increase_status_count status, t if @mail.status?(status)
+ end
+
+ def decrease_status_count_checked(status, t = nil)
+ decrease_status_count status, t if @mail.status?(status)
+ end
+
+ def increase_status_count(status, t = nil)
+ change_all_count 1, t, status
+ end
+
+ def decrease_status_count(status, t = nil)
+ change_all_count -1, t, status
+ end
+
+ def decrease_all(t = nil)
+ decrease_all_count t
+ decrease_status_count_checked :read, t
+ decrease_status_count_checked :starred, t
+ decrease_status_count_checked :replied, t
+ end
+
+ def increase_all(t = nil)
+ increase_all_count t
+ increase_status_count_checked :read, t
+ increase_status_count_checked :starred, t
+ increase_status_count_checked :replied, t
+ end
+
+ def mail=(m)
+ decrease_all if @mail
+ @mail = m
+ increase_all if m
+ end
+
+ def add_tag(name)
+ unless @tags[normalized(name)]
+ t = @tags[normalized(name)] = Tags.get(name)
+ increase_all t
+ end
+ end
+
+ def remove(name)
+ if t = @tags[normalized(name)]
+ @tags.delete(normalized(name))
+ decrease_all t
+ end
+ end
+
+ def added_status(nm)
+ increase_status_count nm
+ end
+
+ def removed_status(nm)
+ decrease_status_count nm
+ end
+
+ def is_tagged?(t)
+ !!@tags[normalized(t.name)]
+ end
+
+ def names
+ @tags.values.sort_by do |v|
+ [SPECIAL.index(normalized(v.name)) || 999, normalized(v.name)]
+ end.map(&:name)
+ end
+
+ def normalized(n)
+ Tags.normalized(n)
+ end
+
+ class <<self
+ def normalized(n)
+ n.downcase
+ end
+
+ def get(name)
+ self.tags[normalized(name)] || create_tag(name)
+ end
+
+ def tags
+ @tags ||= {}
+ end
+
+ def clean
+ @tags = {}
+ create_default_tags
+ @tags
+ end
+
+ def all_tags
+ tags.values.sort_by { |x| normalized(x.name) }
+ end
+
+ def create_tag(name)
+ Smail::Tag.new(name, 0, 0, 0, 0, false).tap do |t|
+ self.tags[normalized(name)] = t
+ end
+ end
+
+ def create_default_tags()
+ SPECIAL.each do |name|
+ self.tags[normalized(name)] = Smail::Tag.new(name, 0, 0, 0, 0, true)
+ end
+ end
+ end
+ end
+end
diff --git a/fake-service/spec/fixtures/mail1 b/fake-service/spec/fixtures/mail1
new file mode 100644
index 00000000..d977bff6
--- /dev/null
+++ b/fake-service/spec/fixtures/mail1
@@ -0,0 +1,15 @@
+Date: Thu, 29 May 2014 18:56:41 -0300
+From: kenneth@willmsmckenzie.org
+To: antonio@beier.biz
+Message-ID: <5387ad199161e_6ced7c32ec77517@norepinephrine.mail>
+Subject: Doloremque aliquid a facilis et sit numquam libero.
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=UTF-8
+Content-Transfer-Encoding: 7bit
+
+Quia tempora quas laboriosam. Dolorem dolor fuga a aut minima sint. Ullam suscipit consectetur nihil. Incidunt velit aut reprehenderit.
+
+Laborum blanditiis praesentium soluta dolorem laudantium a. Molestiae excepturi laudantium at eos velit. Commodi quaerat suscipit laudantium sapiente aut omnis. Qui iure impedit ea dolores. Et debitis non est tempora id autem.
+
+Atque officia architecto sed assumenda. Inventore quia minus. Doloribus amet reiciendis ipsam aut.
diff --git a/fake-service/spec/fixtures/mail2 b/fake-service/spec/fixtures/mail2
new file mode 100644
index 00000000..d1843882
--- /dev/null
+++ b/fake-service/spec/fixtures/mail2
@@ -0,0 +1,16 @@
+Date: Thu, 29 May 2014 18:56:41 -0300
+From: kenneth@willmsmckenzie.org
+To: cmurphy@thoughtworks.com, cgorslin@thoughtworks.com, cmitchel@thoughtworks.com, dnorth@thoughtworks.com, dpgoodwi@thoughtworks.com, dbodart@thoughtworks.com, dsmith@thoughtworks.com, djrice@thoughtworks.com, dwhalley@thoughtworks.com
+CC: amonago@thoughtworks.com, agore@thoughtworks.com, bswamina@thoughtworks.com, baphipps@thoughtworks.com, bbutler@thoughtworks.com, cwathing@thoughtworks.com
+Message-ID: <5387ad199161e_6ced7c32ec77517@norepinephrine.mail>
+Subject: Doloremque aliquid a facilis et sit numquam libero.
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=UTF-8
+Content-Transfer-Encoding: 7bit
+
+Quia tempora quas laboriosam. Dolorem dolor fuga a aut minima sint. Ullam suscipit consectetur nihil. Incidunt velit aut reprehenderit.
+
+Laborum blanditiis praesentium soluta dolorem laudantium a. Molestiae excepturi laudantium at eos velit. Commodi quaerat suscipit laudantium sapiente aut omnis. Qui iure impedit ea dolores. Et debitis non est tempora id autem.
+
+Atque officia architecto sed assumenda. Inventore quia minus. Doloribus amet reiciendis ipsam aut.
diff --git a/fake-service/spec/smail/mail_spec.rb b/fake-service/spec/smail/mail_spec.rb
new file mode 100644
index 00000000..b75ceff5
--- /dev/null
+++ b/fake-service/spec/smail/mail_spec.rb
@@ -0,0 +1,103 @@
+
+fixture1 = File.read(File.join(File.dirname(__FILE__), "..", "fixtures", "mail1"))
+fixture2 = File.read(File.join(File.dirname(__FILE__), "..", "fixtures", "mail2"))
+
+describe Smail::Mail do
+ describe "#read" do
+ context("simple email") do
+ subject(:mail) { Smail::Mail.read(fixture1) }
+
+ it "reads the subject correctly" do
+ expect(mail.subject).to eq("Doloremque aliquid a facilis et sit numquam libero.")
+ end
+
+ it "reads the from correctly" do
+ expect(mail.from).to eq("kenneth@willmsmckenzie.org")
+ end
+
+ it "reads the to correctly" do
+ expect(mail.to).to eq("antonio@beier.biz")
+ end
+
+ it "reads and parses date" do
+ expect(mail.headers[:date]).to eq(DateTime.parse("2014-05-29T18:56:41 -0300"))
+ end
+
+ it "reads the body correctly" do
+ expect(mail.body).to eq(<<BODY)
+Quia tempora quas laboriosam. Dolorem dolor fuga a aut minima sint. Ullam suscipit consectetur nihil. Incidunt velit aut reprehenderit.
+
+Laborum blanditiis praesentium soluta dolorem laudantium a. Molestiae excepturi laudantium at eos velit. Commodi quaerat suscipit laudantium sapiente aut omnis. Qui iure impedit ea dolores. Et debitis non est tempora id autem.
+
+Atque officia architecto sed assumenda. Inventore quia minus. Doloribus amet reiciendis ipsam aut.
+BODY
+ end
+
+ it "reads a multi line header correctly" do
+ expect(mail.headers[:content_type]).to eq("text/plain; charset=UTF-8")
+ end
+ end
+
+ context("with multiple recipients") do
+ subject(:mail) { Smail::Mail.read(fixture2) }
+
+ it "reads multiple recipients correctly" do
+ expect(mail.to).to eq(%w(cmurphy@thoughtworks.com cgorslin@thoughtworks.com cmitchel@thoughtworks.com dnorth@thoughtworks.com dpgoodwi@thoughtworks.com dbodart@thoughtworks.com dsmith@thoughtworks.com djrice@thoughtworks.com dwhalley@thoughtworks.com))
+ end
+
+ it "reads multiple cc recipients correctly" do
+ expect(mail.headers[:cc]).to eq(%w(amonago@thoughtworks.com agore@thoughtworks.com bswamina@thoughtworks.com baphipps@thoughtworks.com bbutler@thoughtworks.com cwathing@thoughtworks.com))
+ end
+ end
+ end
+
+ describe ".to_s" do
+ context("simple email") do
+ subject(:mail) { Smail::Mail.read(fixture1) }
+
+ it "writes correct output" do
+ expect(mail.to_s).to eq(<<MAIL)
+Content-Transfer-Encoding: 7bit
+Content-Type: text/plain; charset=UTF-8
+Date: Thu, 29 May 2014 18:56:41 -0300
+From: kenneth@willmsmckenzie.org
+Message-ID: <5387ad199161e_6ced7c32ec77517@norepinephrine.mail>
+Mime-Version: 1.0
+Subject: Doloremque aliquid a facilis et sit numquam libero.
+To: antonio@beier.biz
+X-TW-SMail-Ident:
+
+Quia tempora quas laboriosam. Dolorem dolor fuga a aut minima sint. Ullam suscipit consectetur nihil. Incidunt velit aut reprehenderit.
+
+Laborum blanditiis praesentium soluta dolorem laudantium a. Molestiae excepturi laudantium at eos velit. Commodi quaerat suscipit laudantium sapiente aut omnis. Qui iure impedit ea dolores. Et debitis non est tempora id autem.
+
+Atque officia architecto sed assumenda. Inventore quia minus. Doloribus amet reiciendis ipsam aut.
+MAIL
+ end
+ end
+ context("with multiple recipients") do
+ subject(:mail) { Smail::Mail.read(fixture2) }
+
+ it "writes correct output" do
+ expect(mail.to_s).to eq(<<MAIL)
+CC: amonago@thoughtworks.com, agore@thoughtworks.com, bswamina@thoughtworks.com, baphipps@thoughtworks.com, bbutler@thoughtworks.com, cwathing@thoughtworks.com
+Content-Transfer-Encoding: 7bit
+Content-Type: text/plain; charset=UTF-8
+Date: Thu, 29 May 2014 18:56:41 -0300
+From: kenneth@willmsmckenzie.org
+Message-ID: <5387ad199161e_6ced7c32ec77517@norepinephrine.mail>
+Mime-Version: 1.0
+Subject: Doloremque aliquid a facilis et sit numquam libero.
+To: cmurphy@thoughtworks.com, cgorslin@thoughtworks.com, cmitchel@thoughtworks.com, dnorth@thoughtworks.com, dpgoodwi@thoughtworks.com, dbodart@thoughtworks.com, dsmith@thoughtworks.com, djrice@thoughtworks.com, dwhalley@thoughtworks.com
+X-TW-SMail-Ident:
+
+Quia tempora quas laboriosam. Dolorem dolor fuga a aut minima sint. Ullam suscipit consectetur nihil. Incidunt velit aut reprehenderit.
+
+Laborum blanditiis praesentium soluta dolorem laudantium a. Molestiae excepturi laudantium at eos velit. Commodi quaerat suscipit laudantium sapiente aut omnis. Qui iure impedit ea dolores. Et debitis non est tempora id autem.
+
+Atque officia architecto sed assumenda. Inventore quia minus. Doloribus amet reiciendis ipsam aut.
+MAIL
+ end
+ end
+ end
+end
diff --git a/fake-service/spec/spec_helper.rb b/fake-service/spec/spec_helper.rb
new file mode 100644
index 00000000..d25e8240
--- /dev/null
+++ b/fake-service/spec/spec_helper.rb
@@ -0,0 +1 @@
+require 'smail'