From e54e5ee931b3991cbb5e427e7e5d27b3f6c75e6e Mon Sep 17 00:00:00 2001 From: Ola Bini Date: Thu, 31 Jul 2014 19:35:40 -0300 Subject: Add fake-service --- fake-service/lib/smail/all.rb | 8 + fake-service/lib/smail/combined_observer.rb | 23 ++ fake-service/lib/smail/contact.rb | 26 ++ fake-service/lib/smail/contacts.rb | 84 ++++++ fake-service/lib/smail/contacts_observer.rb | 54 ++++ fake-service/lib/smail/contacts_search.rb | 71 +++++ fake-service/lib/smail/contacts_sorter.rb | 8 + fake-service/lib/smail/fake.rb | 146 ++++++++++ fake-service/lib/smail/mail.rb | 299 +++++++++++++++++++++ fake-service/lib/smail/mail_scope_filter.rb | 70 +++++ fake-service/lib/smail/mail_service.rb | 152 +++++++++++ fake-service/lib/smail/mailset.rb | 84 ++++++ fake-service/lib/smail/paginate.rb | 15 ++ fake-service/lib/smail/paginated_enumerable.rb | 29 ++ fake-service/lib/smail/persona.rb | 12 + fake-service/lib/smail/search.rb | 133 +++++++++ fake-service/lib/smail/search/and_match.rb | 25 ++ fake-service/lib/smail/search/negate_match.rb | 22 ++ fake-service/lib/smail/search/or_match.rb | 25 ++ fake-service/lib/smail/search/scope_match.rb | 79 ++++++ fake-service/lib/smail/search/string_match.rb | 37 +++ fake-service/lib/smail/search/true_match.rb | 13 + fake-service/lib/smail/security_casing.rb | 55 ++++ fake-service/lib/smail/security_casing_examples.rb | 142 ++++++++++ fake-service/lib/smail/server.rb | 82 ++++++ fake-service/lib/smail/sorted_mail.rb | 57 ++++ fake-service/lib/smail/stats.rb | 60 +++++ fake-service/lib/smail/stats_observer.rb | 19 ++ fake-service/lib/smail/tags.rb | 150 +++++++++++ 29 files changed, 1980 insertions(+) create mode 100644 fake-service/lib/smail/all.rb create mode 100644 fake-service/lib/smail/combined_observer.rb create mode 100644 fake-service/lib/smail/contact.rb create mode 100644 fake-service/lib/smail/contacts.rb create mode 100644 fake-service/lib/smail/contacts_observer.rb create mode 100644 fake-service/lib/smail/contacts_search.rb create mode 100644 fake-service/lib/smail/contacts_sorter.rb create mode 100644 fake-service/lib/smail/fake.rb create mode 100644 fake-service/lib/smail/mail.rb create mode 100644 fake-service/lib/smail/mail_scope_filter.rb create mode 100644 fake-service/lib/smail/mail_service.rb create mode 100644 fake-service/lib/smail/mailset.rb create mode 100644 fake-service/lib/smail/paginate.rb create mode 100644 fake-service/lib/smail/paginated_enumerable.rb create mode 100644 fake-service/lib/smail/persona.rb create mode 100644 fake-service/lib/smail/search.rb create mode 100644 fake-service/lib/smail/search/and_match.rb create mode 100644 fake-service/lib/smail/search/negate_match.rb create mode 100644 fake-service/lib/smail/search/or_match.rb create mode 100644 fake-service/lib/smail/search/scope_match.rb create mode 100644 fake-service/lib/smail/search/string_match.rb create mode 100644 fake-service/lib/smail/search/true_match.rb create mode 100644 fake-service/lib/smail/security_casing.rb create mode 100644 fake-service/lib/smail/security_casing_examples.rb create mode 100644 fake-service/lib/smail/server.rb create mode 100644 fake-service/lib/smail/sorted_mail.rb create mode 100644 fake-service/lib/smail/stats.rb create mode 100644 fake-service/lib/smail/stats_observer.rb create mode 100644 fake-service/lib/smail/tags.rb (limited to 'fake-service/lib/smail') 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 "], ["1905060826932239808", "3264050876889579764"], :valid, 4096, :RSA, :ultimate, :ultimate) + + EXPIRED_UN_UN = SecurityCasing::Key.new("05A63421F637E333", "41EA1D94F26186026CD4B2B505A63421F637E333", ["Rylee Elise Fowler ", + "Rylee Fowler (gmail mail, generally unused) "], [], :expired, 2048, :RSA, :unknown, :unknown) + + REVOKED_F_UN = SecurityCasing::Key.new("11044FD19FC527CC", "E64F19EBBBE86AA97AF36FD511044FD19FC527CC", ["Michael Rogers "], [], :revoked, 2048, :RSA, :full, :unknown) + + EXPIRED_NO_TRUST = SecurityCasing::Key.new("FB3973E142A913A4", "AB465EE7022B68C42DBAD324FB3973E142A913A4", ["Michael Granger ", "Michael Granger ", "Michael Granger ", + "Michael Granger ", "Ged the Grey's Hain ", + "Michael Granger ", "Michael Granger ", + "Michael Granger ", "Michael Granger (at work) "], + [], :expired, 1024, :DSA, :no_trust, :no_trust) + + VALID_NO_TRUST = SecurityCasing::Key.new("29F16F77D77A211F", "692B652B70BC67E8EEA36E0929F16F77D77A211F", ["Christian Trabold ", + "Christian Trabold "], [], :valid, 4096, :RSA, :no_trust, :no_trust) + + EXAMPLES = [ + VALID_U_U, + SecurityCasing::Key.new("37561129CF4BE610", "D37F700C25569B6F1E1286EF37561129CF4BE610", ["Molly "], [], :valid, 4096, :RSA, :ultimate, :no_trust), + EXPIRED_UN_UN, + EXPIRED_NO_TRUST, + SecurityCasing::Key.new("E62030AB4AA41495", "BEB9D9E74B0C5167C5FC6CC8E62030AB4AA41495", ["Christopher Dell "], ["4117763089493997091"], :valid, 2048, :RSA, :unknown, :full), + SecurityCasing::Key.new("D692003DAA02C70A", "3E053E70DE40B13ADE913E7ED692003DAA02C70A", ["Tyler Hicks ", "Tyler Hicks ", "Tyler Hicks ", + "Tyler Hicks ", "Tyler Hicks ", "Tyler Hicks ", + "Tyler Hicks ", "Tyler Hicks "], + ["792826280999687388"], :valid, 4096, :RSA, :marginal, :full), + REVOKED_F_UN, + VALID_NO_TRUST, + SecurityCasing::Key.new("5DFEA1062EA46E4F", "14D07803BFF6EFA099988C4B5DFEA1062EA46E4F", ["Henri Salo ", "Henri Salo ", "Henri Salo ", + "Henri Salo "], [], :valid, 1024, :DSA, :no_trust, :ultimate), + SecurityCasing::Key.new("C44FBF8A41A80850", "EE6497E3FEC3773BAD33062DC44FBF8A41A80850", ["Seeta Gangadharan "], [], :valid, 2048, :RSA, :marginal, :full), + SecurityCasing::Key.new("7934ED27275BDB05", "9A6D46E5E7489C60A1DEFDCA7934ED27275BDB05", ["Ernesto Medina Delgado "], ["2618860400823413108"], :valid, 4096, :RSA, :full, :unknown), + SecurityCasing::Key.new("EC73D77206A7B07F", "728CFCA32AAFF261DE88971CEC73D77206A7B07F", ["Aaron Bedra "], ["1494195372012211732"], :revoked, 1024, :DSA, :ultimate, :ultimate), + SecurityCasing::Key.new("ACD5E501207FBB0E", "281C60C3D20A19C7D2608302ACD5E501207FBB0E", ["Chip Collier "], [], :valid, 2048, :RSA, :full, :ultimate), + SecurityCasing::Key.new("2ECDE8FDF22DB236", "E6D4C474A259113A02E8F2772ECDE8FDF22DB236", ["Hiroshi Nakamura (NaHi) ", "Hiroshi Nakamura (NaHi) ", + "Hiroshi Nakamura (NaHi) "], [], :revoked, 2048, :RSA, :no_trust, :marginal), + SecurityCasing::Key.new("482ECB2BDAAC67D2", "A00620D62EA9B36A3BB71BDE482ECB2BDAAC67D2", ["severino "], ["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 <