You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@whimsical.apache.org by Sam Ruby <ru...@apache.org> on 2015/12/13 16:14:54 UTC
[whimsy.git] [25/37] Commit c797456: delete logic
Commit c797456d861551e09df38cc8f96a856bd609ed89:
delete logic
Branch: refs/heads/secmail
Author: Sam Ruby <ru...@intertwingly.net>
Committer: Sam Ruby <ru...@intertwingly.net>
Pusher: rubys <ru...@apache.org>
------------------------------------------------------------
mailbox.rb | ++++++++ --
parsemail.rb | +++++ ------
server.rb | +++++++++++++++
views/app.js.rb | + -
views/http.js.rb | ++++++++
views/index.html.rb | + ----------
views/index.js.rb | +++++++++ -
views/index.json.rb | ++ --
views/utils.js.rb | ------------
------------------------------------------------------------
395 changes: 259 additions, 136 deletions.
------------------------------------------------------------
diff --git a/mailbox.rb b/mailbox.rb
index 030f960..d914f12 100644
--- a/mailbox.rb
+++ b/mailbox.rb
@@ -18,13 +18,29 @@ def initialize(name)
name = File.basename(name, '.yml') if name.end_with? '.yml'
if name =~ /^\d+$/
- @filename = Dir["#{ARCHIVE}/#{name}", "#{ARCHIVE}/#{name}.gz"].first
+ @filename = Dir["#{ARCHIVE}/#{name}", "#{ARCHIVE}/#{name}.gz"].first.
+ untaint
else
@filename = name
end
end
#
+ # encapsulate updates to a mailbox
+ #
+ def self.update(name)
+ yaml = Mailbox.new(name).yaml_file
+ File.open(yaml, File::RDWR|File::CREAT, 0644) do |file|
+ file.flock(File::LOCK_EX)
+ mbox = YAML.load(file.read) || {} rescue {}
+ yield mbox
+ file.rewind
+ file.write YAML.dump(mbox)
+ file.truncate(file.pos)
+ end
+ end
+
+ #
# Determine whether or not the mailbox exists
#
def exist?
@@ -73,11 +89,19 @@ def each(&block)
end
#
+ # name of associated yaml file
+ #
+ def yaml_file
+ source = File.basename(@filename, '.gz').untaint
+ "#{ARCHIVE}/#{source}.yml"
+ end
+
+ #
# return headers
#
def headers
source = File.basename(@filename, '.gz').untaint
- messages = YAML.load_file("#{ARCHIVE}/#{source}.yml") rescue {}
+ messages = YAML.load_file(yaml_file) rescue {}
messages.delete :mtime
messages.each do |key, value|
value[:source]=source
@@ -98,23 +122,23 @@ def self.headers(part)
# extract all fields from the mail (recovering from bad encoding issues)
fields = part.header_fields.map do |field|
begin
- next [field.name, field.to_s] if field.to_s.valid_encoding?
+ next [field.name, field.to_s] if field.to_s.valid_encoding?
rescue
end
if field.value and field.value.valid_encoding?
- [field.name, field.value]
+ [field.name, field.value]
else
- [field.name, field.value.inspect]
+ [field.name, field.value.inspect]
end
end
# group fields by name
fields = fields.group_by(&:first).map do |name, values|
if values.length == 1
- [name, values.first.last]
+ [name, values.first.last]
else
- [name, values.map(&:last)]
+ [name, values.map(&:last)]
end
end
diff --git a/parsemail.rb b/parsemail.rb
index be4f846..cefa47d 100644
--- a/parsemail.rb
+++ b/parsemail.rb
@@ -37,9 +37,7 @@
next if mbox[:mtime] == File.mtime(name)
# open the YAML file for real (locking it this time)
- File.open(yaml, File::RDWR|File::CREAT, 0644) do |file|
- file.flock(File::LOCK_EX)
- mbox = YAML.load_file(yaml) || {} rescue {}
+ Mailbox.update(name) do |mbox|
mbox[:mtime] = File.mtime(name)
# read (and unzip) the mailbox
@@ -54,9 +52,9 @@
# parse from address
begin
- from = Mail::Address.new(mail[:from].value).display_name
+ from = Mail::Address.new(mail[:from].value).display_name
rescue Exception
- from = mail[:from].value
+ from = mail[:from].value
end
# determine who should be copied on any responses
@@ -66,19 +64,19 @@
# remove secretary and anybody on the to field from the cc list
cc.reject! do |email|
- begin
- address = Mail::Address.new(email).address
- return true if address == 'secretary@apache.org'
- return true if mail.from_addrs.include? address
- rescue Exception
- true
- end
+ begin
+ address = Mail::Address.new(email).address
+ return true if address == 'secretary@apache.org'
+ return true if mail.from_addrs.include? address
+ rescue Exception
+ true
+ end
end
# start an entry for this mail
mbox[id] = {
- from: mail.from_addrs.first,
- name: from,
+ from: mail.from_addrs.first,
+ name: from,
time: (mail.date.to_time.gmtime.iso8601 rescue nil),
cc: cc
}
@@ -88,11 +86,11 @@
# add in attachments
if mail.attachments.length > 0
- attachments = mail.attachments.map do |attach|
- description = {
+ attachments = mail.attachments.map do |attach|
+ description = {
name: attach.filename,
length: attach.body.to_s.length,
- mime: attach.mime_type
+ mime: attach.mime_type
}
if description[:name].empty? and attach['Content-ID']
@@ -100,14 +98,11 @@
end
description.merge(Mailbox.headers(attach))
- end
+ end
- mbox[id][:attachments] = attachments
+ mbox[id][:attachments] = attachments
end
end
-
- # update YAML file
- YAML.dump(mbox, file)
end
end
diff --git a/server.rb b/server.rb
index 52f85b1..1c565bc 100644
--- a/server.rb
+++ b/server.rb
@@ -30,6 +30,21 @@
_html :message
end
+# a single message
+delete %r{^/(\d+)/(\w+)/$} do |month, hash|
+ success = false
+
+ Mailbox.update(month) do |headers|
+ if headers[hash]
+ headers[hash][:status] = :deleted
+ success = true
+ end
+ end
+
+ pass unless success
+ _json success: true
+end
+
# list of parts for a single message
get %r{^/(\d+)/(\w+)/_index_$} do |month, hash|
@message = Mailbox.new(month).headers[hash]
diff --git a/views/app.js.rb b/views/app.js.rb
index 6a6bd2f..5906d9c 100644
--- a/views/app.js.rb
+++ b/views/app.js.rb
@@ -1,4 +1,4 @@
-require_relative 'utils'
+require_relative 'http'
require_relative 'index'
require_relative 'parts'
diff --git a/views/http.js.rb b/views/http.js.rb
new file mode 100644
index 0000000..4934d57
--- /dev/null
+++ b/views/http.js.rb
@@ -0,0 +1,127 @@
+#
+# Encapsulations for asynchronous HTTP requests. Uses older XMLHttpRequest
+# API over fetch as fetch isn't widely supported yet:
+# https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API#Browser_compatibility
+#
+
+
+class HTTP
+ # "AJAX" style post request to the server, with a callback
+ def self.post(target, data, &block)
+ xhr = XMLHttpRequest.new()
+ xhr.open('POST', target, true)
+ xhr.setRequestHeader('Content-Type', 'application/json;charset=utf-8')
+ xhr.responseType = 'text'
+
+ def xhr.onreadystatechange()
+ if xhr.readyState == 4
+ data = nil
+
+ begin
+ if xhr.status == 200
+ data = JSON.parse(xhr.responseText)
+ alert "Exception\n#{data.exception}" if data.exception
+ elsif xhr.status == 404
+ alert "Not Found: #{target}"
+ elsif xhr.status >= 400
+ console.log(xhr.response)
+ if not xhr.response
+ alert "Exception - #{xhr.statusText}"
+ elsif xhr.response.exception
+ alert "Exception\n#{xhr.response.exception}"
+ else
+ alert "Exception\n#{JSON.parse(xhr.responseText).exception}"
+ end
+ end
+ rescue => e
+ console.log(e)
+ end
+
+ block(data)
+ end
+ end
+
+ xhr.send(JSON.stringify(data))
+ end
+
+ # "AJAX" style delete request to the server, with a callback
+ def self.delete(target, &block)
+ xhr = XMLHttpRequest.new()
+ xhr.open('DELETE', target, true)
+
+ def xhr.onreadystatechange()
+ if xhr.readyState == 4
+
+ begin
+ if xhr.status == 404
+ alert "Not Found: #{target}"
+ elsif xhr.status >= 400
+ console.log(xhr.response)
+ if not xhr.response
+ alert "Exception - #{xhr.statusText}"
+ elsif xhr.response.exception
+ alert "Exception\n#{xhr.response.exception}"
+ else
+ alert "Exception\n#{JSON.parse(xhr.responseText).exception}"
+ end
+ end
+ rescue => e
+ console.log(e)
+ end
+
+ block()
+ end
+ end
+
+ xhr.send()
+ end
+
+ # "AJAX" style get request to the server, with a callback
+ def self.ge(target, type, &block)
+ xhr = XMLHttpRequest.new()
+
+ def xhr.onreadystatechange()
+ if xhr.readyState == 1
+ clock_counter += 1
+ setTimeout(0) {Main.refresh()}
+ elsif xhr.readyState == 4
+ data = nil
+
+ begin
+ if xhr.status == 200
+ if type == :json
+ data = xhr.response || JSON.parse(xhr.responseText)
+ else
+ data = xhr.responseText
+ end
+ elsif xhr.status == 404
+ alert "Not Found: #{type}/#{target}"
+ elsif xhr.status >= 400
+ console.log(xhr.response)
+ if not xhr.response
+ alert "Exception - #{xhr.statusText}"
+ elsif xhr.response.exception
+ alert "Exception\n#{xhr.response.exception}"
+ else
+ alert "Exception\n#{JSON.parse(xhr.responseText).exception}"
+ end
+ end
+ rescue => e
+ console.log(e)
+ end
+
+ block(data)
+ clock_counter -= 1
+ end
+ end
+
+ if target =~ /^https?:/
+ xhr.open('GET', target, true)
+ xhr.setRequestHeader("Accept", "application/json") if type == :json
+ else
+ xhr.open('GET', "../#{type}/#{target}", true)
+ end
+ xhr.responseType = type
+ xhr.send()
+ end
+end
diff --git a/views/index.html.rb b/views/index.html.rb
index 554e5c6..51ecb2f 100644
--- a/views/index.html.rb
+++ b/views/index.html.rb
@@ -1,14 +1,5 @@
_html do
- _style %{
- td:nth-child(2), th:nth-child(2) {
- padding-right: 7px;
- padding-left: 7px;
- }
-
- .selected {
- background-color: yellow
- }
- }
+ _link rel: 'stylesheet', type: 'text/css', href: 'secmail.css'
_div.index!
diff --git a/views/index.js.rb b/views/index.js.rb
index a48480d..a72ad0a 100644
--- a/views/index.js.rb
+++ b/views/index.js.rb
@@ -6,6 +6,7 @@ class Index < React
def initialize
@selected = nil
@messages = []
+ @undoStack = []
end
def render
@@ -20,7 +21,14 @@ def render
_tbody do
@messages.each do |message|
- _tr class: ('selected' if message.href == @selected) do
+
+ # determine the 'color' to use for the row
+ color = nil
+ color = 'deleted' if message.status == :deletePending
+ color = 'hidden' if message.status == :deleted
+ color = 'selected' if message.href == @selected
+
+ _tr class: color, onClick: self.selectRow, onDoubleClick: self.nav do
_td do
_a message.time, href: "#{message.href}"
end
@@ -46,17 +54,25 @@ def componentDidMount()
window.onkeydown = self.keydown
end
- # when content changes, scroll to selected message
+ # when content changes, ensure selected message is visible
def componentDidUpdate()
if @selected
selected = document.querySelector("a[href='#{@selected}']")
- selected.scrollIntoView() if selected
+ if selected
+ rect = selected.getBoundingClientRect()
+ if
+ rect.top < 0 or rect.left < 0 or
+ rect.bottom > window.innerHeight or rect.right > window.innerWidth
+ then
+ selected.scrollIntoView()
+ end
+ end
end
end
# fetch a month's worth of messages
def fetch_month()
- post('', mbox: @latest) do |response|
+ HTTP.post('', mbox: @latest) do |response|
# update latest mbox
@latest = response.mbox if response.mbox
@@ -68,18 +84,62 @@ def fetch_month()
end
end
+ # select
+ def selectRow(event)
+ @selected = event.currentTarget.querySelector('a').getAttribute('href')
+ end
+
+ # navigate
+ def nav(event)
+ @selected = event.currentTarget.querySelector('a').getAttribute('href')
+ window.location.href = @selected
+ window.getSelection().removeAllRanges()
+ event.preventDefault()
+ end
+
# handle keyboard events
def keydown(event)
if event.keyCode == 38 # up
index = @messages.findIndex {|m| return m.href == @selected}
@selected = @messages[index-1].href if index > 0
event.preventDefault()
+
elsif event.keyCode == 40 # down
index = @messages.findIndex {|m| return m.href == @selected}
@selected = @messages[index+1].href if index < @messages.length-1
event.preventDefault()
+
+ elsif event.keyCode == 13 or event.keyCode == 39 # enter/return or right
+ selected = @messages.find {|m| return m.href == @selected}
+ window.location.href = selected.href if selected
+
+ elsif event.keyCode == 8 # delete
+ event.preventDefault()
+ # mark item as delete pending
+ selected = @selected
+ index = @messages.findIndex {|m| return m.href == selected}
+ @messages[index].status = :deletePending if index >= 0
+
+ # move selected pointer
+ if index > 0
+ @selected = @messages[index-1].href
+ elsif index < @messages.length - 1
+ @selected = @messages[index+1].href
+ else
+ @selected = nil
+ end
+
+ # send request to server to perform delete
+ HTTP.delete(selected) do
+ index = @messages.findIndex {|m| return m.href == selected}
+ console.log index
+ @messages[index].status = :deleted if index >= 0
+ @undoStack << selected
+ self.forceUpdate()
+ end
+
else
- alert event.keyCode
+ console.log "keydown: #{event.keyCode}"
end
end
end
diff --git a/views/index.json.rb b/views/index.json.rb
index 77a3a9a..f373acf 100644
--- a/views/index.json.rb
+++ b/views/index.json.rb
@@ -3,11 +3,11 @@
available = Dir["#{ARCHIVE}/*.yml"].sort
index = available.find_index "#{ARCHIVE}/#{@mbox}.yml"
- # if found and not first, process it
+ # if found, process it
if index
# fetch a list of headers for all messages in the maibox with attachments
headers = Mailbox.new(@mbox).headers.to_a.select do |id, message|
- message[:attachments]
+ message[:attachments] and not message[:status] == :deleted
end
# extract relevant fields from the headers
diff --git a/views/utils.js.rb b/views/utils.js.rb
deleted file mode 100644
index f8db8eb..0000000
--- a/views/utils.js.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-# "AJAX" style post request to the server, with a callback
-def post(target, data, &block)
- xhr = XMLHttpRequest.new()
- xhr.open('POST', target, true)
- xhr.setRequestHeader('Content-Type', 'application/json;charset=utf-8')
- xhr.responseType = 'text'
-
- def xhr.onreadystatechange()
- if xhr.readyState == 4
- data = nil
-
- begin
- if xhr.status == 200
- data = JSON.parse(xhr.responseText)
- alert "Exception\n#{data.exception}" if data.exception
- elsif xhr.status == 404
- alert "Not Found: json/#{target}"
- elsif xhr.status >= 400
- console.log(xhr.response)
- if not xhr.response
- alert "Exception - #{xhr.statusText}"
- elsif xhr.response.exception
- alert "Exception\n#{xhr.response.exception}"
- else
- alert "Exception\n#{JSON.parse(xhr.responseText).exception}"
- end
- end
- rescue => e
- console.log(e)
- end
-
- block(data)
- Main.refresh()
- end
- end
-
- xhr.send(JSON.stringify(data))
-end
-
-# "AJAX" style get request to the server, with a callback
-def fetch(target, type, &block)
- xhr = XMLHttpRequest.new()
-
- def xhr.onreadystatechange()
- if xhr.readyState == 1
- clock_counter += 1
- setTimeout(0) {Main.refresh()}
- elsif xhr.readyState == 4
- data = nil
-
- begin
- if xhr.status == 200
- if type == :json
- data = xhr.response || JSON.parse(xhr.responseText)
- else
- data = xhr.responseText
- end
- elsif xhr.status == 404
- alert "Not Found: #{type}/#{target}"
- elsif xhr.status >= 400
- console.log(xhr.response)
- if not xhr.response
- alert "Exception - #{xhr.statusText}"
- elsif xhr.response.exception
- alert "Exception\n#{xhr.response.exception}"
- else
- alert "Exception\n#{JSON.parse(xhr.responseText).exception}"
- end
- end
- rescue => e
- console.log(e)
- end
-
- block(data)
- clock_counter -= 1
- Main.refresh()
- end
- end
-
- if target =~ /^https?:/
- xhr.open('GET', target, true)
- xhr.setRequestHeader("Accept", "application/json") if type == :json
- else
- xhr.open('GET', "../#{type}/#{target}", true)
- end
- xhr.responseType = type
- xhr.send()
-end
-