summaryrefslogtreecommitdiff
path: root/fake-service/lib
diff options
context:
space:
mode:
authorOla Bini <ola.bini@gmail.com>2014-07-31 19:35:40 -0300
committerOla Bini <ola.bini@gmail.com>2014-07-31 19:35:40 -0300
commite54e5ee931b3991cbb5e427e7e5d27b3f6c75e6e (patch)
tree1e0da33d22874c0ea5576818fe45958611ebda29 /fake-service/lib
parent04cf441c5ae18400c6b4865b0b37a71718dc9d46 (diff)
Add fake-service
Diffstat (limited to 'fake-service/lib')
-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
31 files changed, 2103 insertions, 0 deletions
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