├── utils ├── lib │ ├── __init__.py │ ├── utils.py │ └── objects.py ├── getOrphanEmails.sh ├── getNamedEmails.sh ├── clean-names ├── index.py └── getgraph ├── www ├── css │ ├── style.css │ ├── hypertree.css │ └── infovis.css ├── contacts.php ├── cnet.php ├── org.php ├── contact.php ├── snet.php ├── details.php └── maelstrom.php ├── example.cfg ├── README └── COPYING /utils/lib/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # (C) 2008-2009 by Marsiske Stefan, 3 | __all__ = ["objects","utils"] 4 | -------------------------------------------------------------------------------- /utils/getOrphanEmails.sh: -------------------------------------------------------------------------------- 1 | # Maelstrom - visualizing email contacts 2 | # Copyright© 2008-2009 Stefan Marsiske 3 | echo "select * from email where owner_id is null;" | sqlite3 db/messages.db | sed -s 's/.*|\(.*\)|\(.*\)|/\1@\2/' 4 | -------------------------------------------------------------------------------- /utils/getNamedEmails.sh: -------------------------------------------------------------------------------- 1 | # Maelstrom - visualizing email contacts 2 | # Copyright© 2008-2009 Stefan Marsiske 3 | 4 | echo "select username, mailserver, fullname from email, person where owner_id==person.id;" | sqlite3 db/messages.db | sed -s 's/\(.*\)|\(.*\)|/\1@\2 / ' 5 | -------------------------------------------------------------------------------- /www/css/style.css: -------------------------------------------------------------------------------- 1 | /* Maelstrom - visualizing email contacts 2 | Copyright© 2008-2009 Stefan Marsiske */ 3 | #log { 4 | position:absolute; 5 | top:0; 6 | right:0; 7 | font-weight: bold; 8 | font-size: 1em; 9 | margin: 0; 10 | color: #9FD4FF; 11 | padding: 8px 4px; 12 | background-color:#222; 13 | z-index:3000; 14 | } 15 | -------------------------------------------------------------------------------- /example.cfg: -------------------------------------------------------------------------------- 1 | [maelstrom] 2 | # which database to use 3 | database=../db/messages.db 4 | # map mails and different transcriptions to a canonical or anonymized 5 | # name. 6 | personmapfile=../db/persons.map 7 | # dot|graphxml|log|csv 8 | format=csv 9 | # display overall stats 10 | stats=True 11 | # filter out entity 12 | egg=True 13 | # whom to filter out 14 | egger=Joe Doe 15 | -------------------------------------------------------------------------------- /www/css/hypertree.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | width:100%; 3 | height:100%; 4 | background-color:#444; 5 | text-align:center; 6 | overflow:hidden; 7 | font-size:10px; 8 | font-family:Verdana, Geneva, Arial, Helvetica, sans-serif; 9 | } 10 | 11 | #infovis { 12 | background-color:#222; 13 | width:900px; 14 | height:500px; 15 | } 16 | 17 | .node { 18 | /*border: 1px solid #555;*/ 19 | background-color: #222; 20 | color:yellow; 21 | font-weight:bold; 22 | cursor:pointer; 23 | padding:2px; 24 | } 25 | -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | Maelstrom - visualizing email contacts 2 | 3 | Copyright© 2008-2009 Stefan Marsiske 4 | 5 | License 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU Affero General Public License as 8 | published by the Free Software Foundation, either version 3 of the 9 | License, or (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU Affero General Public License for more details. 15 | 16 | You should have received a copy of the GNU Affero General Public License 17 | along with this program. If not, see . 18 | 19 | Depends 20 | ... on a lot of stuff, like: 21 | * www/timecloud [https://www.ohloh.net/projects/timecloud] 22 | * python, 23 | * php, 24 | * javascript, 25 | * sqlite3, 26 | 27 | HOWTO 28 | 0. Create a db/ directory in the root of the maelstrom distribution. 29 | 1. Get utils/indexer.py to run on your mbox or cyrus mail files. This will 30 | create a message.db sqlite database containing all information from the 31 | headers of the mails parsed. 32 | 2. Configure an apache/php stack to the stuff in the www directory. 33 | 3. If the mails all belong to one person, you should set the MAILBOXOWNER 34 | variable in www/maelstrom.php 35 | 4. direct your browser to http:///contacts.php 36 | or to http:///cnet.php?c= 37 | 38 | have fun, s. 39 | -------------------------------------------------------------------------------- /www/contacts.php: -------------------------------------------------------------------------------- 1 | */ 4 | $timeconstraint=''; 5 | if(isset($_GET['start'])) { 6 | $timeconstraint="&start=".$_GET['start']; 7 | } 8 | if(isset($_GET['end'])) { 9 | $timeconstraint.="&end=".$_GET['end']; 10 | } 11 | ?> 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 25 | 33 | 34 | 35 | 36 |
37 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /www/cnet.php: -------------------------------------------------------------------------------- 1 | */ 4 | if(isset($_GET['c'])) { 5 | $person=$_GET['c']; 6 | } else { 7 | die; 8 | } 9 | $timeconstraint=''; 10 | if(isset($_GET['start'])) { 11 | $timeconstraint="&start=".$_GET['start']; 12 | } 13 | if(isset($_GET['end'])) { 14 | $timeconstraint.="&end=".$_GET['end']; 15 | } 16 | ?> 17 | 18 | 19 | 20 | 21 | 22 | 23 | Contact network 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 39 | 40 | 41 | 42 | ');"> 43 | 44 | 48 | 49 |
50 |
Details
51 |
52 |
53 |
54 |
55 | 56 | 57 |
58 |
59 |
60 |
61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /www/org.php: -------------------------------------------------------------------------------- 1 | */ 4 | include_once("maelstrom.php"); 5 | 6 | if(isset($_GET['o'])) { 7 | $org=$_GET['o']; 8 | } else { 9 | print "please supply an '?o=' http get param"; 10 | die; 11 | } 12 | ?> 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 33 | 37 | 38 | 39 | 40 |
41 | 45 |
46 |
47 |
48 | 49 | 50 | -------------------------------------------------------------------------------- /www/contact.php: -------------------------------------------------------------------------------- 1 | */ 4 | include_once("maelstrom.php"); 5 | 6 | if(isset($_GET['c'])) { 7 | $person=$_GET['c']; 8 | } else { 9 | $person=$MAILBOXOWNER; 10 | } 11 | ?> 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 38 | 42 | 43 | 44 | 45 |
46 | 50 |
51 |
52 |
53 |
54 | 55 | 56 | -------------------------------------------------------------------------------- /www/css/infovis.css: -------------------------------------------------------------------------------- 1 | /******* GENERAL *******/ 2 | html, body { 3 | background-color:#1D1D20; 4 | margin: 0; padding: 0; 5 | text-align:center; 6 | font-family:Verdana, Geneva, Arial, Helvetica, sans-serif; 7 | font-size:11px; 8 | overflow:hidden; 9 | } 10 | 11 | 12 | /****** MAIN LAYOUT ******/ 13 | #header { 14 | width:100%; 15 | height:0px; 16 | } 17 | 18 | #left { 19 | width:250px; 20 | float:left; 21 | margin:0; 22 | text-align:justify; 23 | font-family:Tahoma; 24 | font-size:12px; 25 | overflow-y:hidden; 26 | overflow-x:hidden; 27 | background-color:white; 28 | } 29 | 30 | .inner{ 31 | margin-top:12px; 32 | padding: 0px; margin:0; 33 | height:100%; 34 | overflow:auto; 35 | font-size:11px; 36 | } 37 | 38 | .inner ul { 39 | list-style:none; 40 | padding:0px; 41 | } 42 | 43 | .inner ul li { 44 | padding:1px 5px; 45 | } 46 | 47 | .inner ul li .relation { 48 | font-size:10px; 49 | text-indent:10px; 50 | font-style:italic; 51 | margin:5px 0; 52 | } 53 | 54 | /******* ACCORDION ******/ 55 | .small-title { 56 | background-color:#555; 57 | margin:0; 58 | color:white; 59 | padding:5px; 60 | font-weight:bold; 61 | border-bottom:4px solid #2A2A2F; 62 | } 63 | 64 | #left .left-item { 65 | background-color:#7389AE; 66 | color:white; 67 | padding:5px; 68 | border-bottom:4px solid #2A2A2F; 69 | font-weight:bold; 70 | } 71 | 72 | #left .contained-item { 73 | width:230px; 74 | background-color:white; 75 | color:#555; 76 | padding:10px; 77 | overflow-y:auto; 78 | background-image:url(../img/text-bg.gif); 79 | background-position:top; 80 | background-repeat:repeat-x; 81 | } 82 | 83 | /***** FORM SETTINGS *****/ 84 | 85 | #settings-form { 86 | text-align:right; 87 | padding:7px; 88 | background-color:#2A2A2F; 89 | border:1px solid #2D2D32; 90 | margin-top:6px; 91 | padding-top:18px; 92 | color:white; 93 | height:85%; 94 | } 95 | 96 | #settings-form table tr td { 97 | padding:2px; 98 | } 99 | 100 | #settings-form input, #settings-form select { 101 | font-family:Verdana, Geneva, Arial, Helvetica, sans-serif; 102 | font-size:9px; 103 | color:#444; 104 | width:80px; 105 | border:1px solid #ccc; 106 | } 107 | 108 | /***** SELECTED PATH *****/ 109 | .path{ 110 | padding:10px; 111 | color:white; 112 | font-size:13px; 113 | background-color:#2A2A2F; 114 | text-align:center; 115 | margin-bottom:13px; 116 | } 117 | -------------------------------------------------------------------------------- /utils/lib/utils.py: -------------------------------------------------------------------------------- 1 | """ 2 | Maelstrom - visualizing email contacts 3 | 4 | Copyright(c) 2008-2009 Stefan Marsiske 5 | """ 6 | 7 | import email 8 | import csv 9 | import cStringIO 10 | import codecs 11 | import ConfigParser 12 | import os 13 | 14 | # config object holding our context 15 | CFG = ConfigParser.ConfigParser() 16 | CFG.read(['/var/www/maelstrom/maelstrom.cfg', os.path.expanduser('~/.maelstrom.cfg')]) 17 | 18 | def decode_header(text): 19 | """Decode a header value and return the value as a unicode string.""" 20 | if not text: 21 | return text 22 | res = [] 23 | for part, charset in email.Header.decode_header(text): 24 | try: 25 | res.append(part.decode(charset or 'latin1', 'replace')) 26 | except LookupError: # If charset is unknown 27 | res.append(part.decode('latin1', 'replace')) 28 | return ' '.join(res) 29 | 30 | 31 | class Obj: 32 | """ 33 | abstract baseclass for node,edge,graph 34 | """ 35 | def __getattr__(self, name): 36 | if(self.__dict__.has_key(name)): 37 | return self.__dict__[name] 38 | else: 39 | raise AttributeError, name 40 | 41 | def __setattr__(self, name, value): 42 | if(self.__dict__.has_key(name)): 43 | self.__dict__[name] = value 44 | else: 45 | raise AttributeError, name 46 | 47 | def __repr__(self): 48 | return self.__str__() 49 | 50 | def __str__(self): 51 | return reduce(lambda y, x: "%s%s: %s\n" % (y, 52 | x, 53 | repr(self.__dict__[x])), 54 | self.__dict__.keys()) 55 | 56 | class UnicodeWriter: 57 | """ 58 | A CSV writer which will write rows to CSV file "f", 59 | which is encoded in the given encoding. 60 | """ 61 | def __init__(self, f, dialect=csv.excel, encoding="utf-8", **kwds): 62 | # Redirect output to a queue 63 | self.queue = cStringIO.StringIO() 64 | self.writer = csv.writer(self.queue, dialect=dialect, **kwds) 65 | self.stream = f 66 | self.encoder = codecs.getincrementalencoder(encoding)() 67 | 68 | def writerow(self, row): 69 | #ORIG:self.writer.writerow([s.encode("utf-8") for s in row]) 70 | self.writer.writerow(row) 71 | # Fetch UTF-8 output from the queue ... 72 | data = self.queue.getvalue() 73 | data = data.decode("utf-8") 74 | # ... and reencode it into the target encoding 75 | data = self.encoder.encode(data) 76 | # write to the target stream 77 | self.stream.write(data) 78 | # empty queue 79 | self.queue.truncate(0) 80 | 81 | 82 | def counter(start=0): 83 | """ 84 | auto incrementing id generator 85 | """ 86 | while True: 87 | start += 1 88 | yield start 89 | -------------------------------------------------------------------------------- /www/snet.php: -------------------------------------------------------------------------------- 1 | */ 4 | 5 | $dburl='sqlite:'.$_SERVER['DOCUMENT_ROOT'].'/maelstrom/db/messages.db'; 6 | 7 | // get all mails from person 8 | 9 | function mailTimeFrame($db) { 10 | $q='SELECT max(delivered) AS max, min(delivered) as min FROM message'; 11 | $result=$db->query($q)->fetch(); 12 | $start=split(" ",$result['min']); 13 | $start=$start[0]; 14 | $end=split(" ",$result['max']); 15 | $end=$end[0]; 16 | return array($start,$end); 17 | } 18 | 19 | function makeNode($name/*, $weight=Array()*/) { 20 | return Array("id" => $name 21 | ,"name" => $name 22 | ,"children" => array() 23 | ,"data" => array(array("key" => "weight", "value" => 0)) 24 | ); 25 | } 26 | 27 | function makeTree($db,$name,$level=2/*,$weight=Array()*/) { 28 | $node=makeNode($name/*,$weight*/); 29 | if($level>0) { 30 | $cl=getContacts($db,$name); 31 | foreach($cl as $c) { 32 | $node["children"][]=makeTree($db,$c["name"],$level-1/*,$c["weight"]*/); 33 | } 34 | } 35 | return $node; 36 | } 37 | 38 | function getContacts($db,$person) { 39 | list($start, $end)=mailTimeFrame($db); 40 | 41 | if(isset($_GET['start'])) { 42 | $start=$_GET['start']; 43 | } 44 | if(isset($_GET['end'])) { 45 | $end=$_GET['end']; 46 | } 47 | 48 | // TODO add window handling if needed, should be enough if the subquery 49 | // return messages in the interval 50 | //and date(message.delivered)>='$start' AND 51 | //date(message.delivered)<'$end'"; 52 | $q="select distinct person.fullname as contact 53 | from role, email, person 54 | where role.msg_id in (select message.id 55 | from message, role, email, person 56 | where message.id==role.msg_id and 57 | role.email_id==email.id and 58 | email.owner_id==person.id and 59 | person.fullname like '$person' and 60 | date(message.delivered)>='$start' AND 61 | date(message.delivered)<'$end') and 62 | role.email_id==email.id and 63 | email.owner_id==person.id and 64 | person.fullname not like '$person';"; 65 | $results=array(); 66 | $r=$db->query($q); 67 | foreach ($r as $row) { 68 | $results[]=Array("name" => $row['contact'] 69 | /*,"weight" => $row['weight']*/); 70 | } 71 | return ($results); 72 | } 73 | 74 | try { 75 | $db= new PDO($dburl); 76 | } 77 | catch( PDOException $exception ){ 78 | die($exception->getMessage()); 79 | } 80 | 81 | if(!isset($_GET['c'])) { 82 | die; 83 | } 84 | $c=$_GET['c']; 85 | 86 | //header("Content-type: text/plain"); 87 | print json_encode(makeTree($db,$c)); 88 | ?> 89 | -------------------------------------------------------------------------------- /utils/lib/objects.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | Maelstrom - visualizing email contacts 4 | 5 | Copyright(c) 2008-2009 Stefan Marsiske 6 | 7 | database layer for maelstrom 8 | """ 9 | 10 | import sqlobject 11 | import sys, os, platform 12 | if(platform.machine()=='i686'): 13 | import psyco 14 | 15 | from utils import CFG 16 | DBPATH = CFG.get('maelstrom','database') 17 | sqlobject.sqlhub.processConnection = sqlobject.connectionForURI('sqlite:' + DBPATH) 18 | 19 | class Message(sqlobject.SQLObject): 20 | """ represents a message object """ 21 | delivered = sqlobject.col.DateTimeCol() 22 | messageid = sqlobject.col.StringCol() 23 | headers = sqlobject.SQLMultipleJoin("HeaderValue") 24 | sender = sqlobject.col.ForeignKey("Email") 25 | path = sqlobject.col.StringCol() 26 | # TODO: if mailindexer 27 | # add path to raw 28 | # add paths to payloads 29 | # add path to mbox where message is stored 30 | 31 | class Header(sqlobject.SQLObject): 32 | """ Represents a header object, this is stored uniquely in a 33 | separate table, headervalues reference these""" 34 | name = sqlobject.col.StringCol(unique = True) 35 | 36 | class Email(sqlobject.SQLObject): 37 | """ represents a email object, it consists of an 38 | @ and an associated owner""" 39 | username = sqlobject.col.StringCol() 40 | mailserver = sqlobject.col.StringCol() 41 | owner = sqlobject.col.ForeignKey('Person') 42 | def getname(self): 43 | """ 44 | returns the most specific name for an email correspondent 45 | """ 46 | if(self.owner): 47 | return self.owner.fullname 48 | return self.username+"@"+self.mailserver 49 | 50 | class Person(sqlobject.SQLObject): 51 | """ represents a person, currently only stores the name""" 52 | fullname = sqlobject.col.StringCol() 53 | 54 | class Role(sqlobject.SQLObject): 55 | """ represents the role of a person in respect to an email, we 56 | link a message, with an email address and the according header 57 | (cc, to)""" 58 | email = sqlobject.col.ForeignKey('Email') 59 | header = sqlobject.col.ForeignKey('Header') 60 | msg = sqlobject.col.ForeignKey('Message') 61 | 62 | class HeaderValue(sqlobject.SQLObject): 63 | """ this represents a header set in a message, the msg is linked 64 | to the header and a value is associated.""" 65 | value = sqlobject.col.StringCol() 66 | msg = sqlobject.col.ForeignKey('Message') 67 | header = sqlobject.col.ForeignKey('Header') 68 | 69 | """ if being executed instead of loaded as a module, create a new 70 | database""" 71 | if (__name__ == '__main__'): 72 | def main(): 73 | """ this function creates a new database""" 74 | Header.createTable(ifNotExists = True) 75 | HeaderValue.createTable(ifNotExists = True) 76 | Person.createTable(ifNotExists = True) 77 | Email.createTable(ifNotExists = True) 78 | Role.createTable(ifNotExists = True) 79 | Message.createTable(ifNotExists = True) 80 | 81 | psyco.full() 82 | sys.exit(main()) 83 | -------------------------------------------------------------------------------- /utils/clean-names: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | Maelstrom - visualizing email contacts 4 | Copyright(c) 2009 Stefan Marsiske 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | updates all email owners based on a simple mapping file 20 | the fullname of the person might be a pseudonym, so you can publish 21 | the results, without affecting the privacy of your peers. 22 | 23 | the mapfile should contain lines starting with an email followed by the full name:" 24 | john@example.com John Doe" 25 | jane@example.com Jane Smith" 26 | """ 27 | 28 | import sys 29 | import os 30 | import getopt 31 | import platform 32 | if(platform.machine()=='i686'): 33 | import psyco 34 | 35 | from lib.objects import Person, Email 36 | 37 | def usage(): 38 | """Prints out the --help""" 39 | print "usage: %s " % (sys.argv[0]) 40 | print "\t-h This Help" 41 | print "\t-d |--database== Database." 42 | print 43 | print "the mapfile should contain lines starting with an email followed by the full name:" 44 | print "john@example.com John Doe" 45 | print "jane@example.com Jane Smith" 46 | 47 | def cleanNames(fname): 48 | if(os.path.exists(fname)): 49 | fp = open(fname,'r') 50 | while(fp): 51 | line = fp.readline() 52 | if not line: 53 | break 54 | (email, name) = line.split(" ", 1) 55 | dbitem=getEmail(email) 56 | if not dbitem: continue 57 | if dbitem.owner and not dbitem.owner.fullname==name: 58 | dbitem.owner.fullname=name.strip() 59 | else: 60 | dbitem.owner=Person(fullname=name.strip()) 61 | fp.close() 62 | 63 | def getPerson(name): 64 | q=Person.select(Person.q.fullname==name) 65 | try: 66 | return q.getOne() 67 | except(SQLObjectNotFound): 68 | return Person(fullname=name) 69 | 70 | def getEmail(address): 71 | name,domain=address.split('@',1) 72 | q=Email.selectBy(username=name, mailserver=domain) 73 | try: 74 | return q.getOne() 75 | except: 76 | print "warning:",address 77 | return None 78 | 79 | def init(): 80 | """ 81 | """ 82 | try: 83 | opts, args = getopt.gnu_getopt( 84 | sys.argv[1:], "hd:", 85 | ["help", "file=", "database="]) 86 | except getopt.GetoptError: 87 | usage() 88 | sys.exit(2) 89 | for o, a in opts: 90 | if o in ("-h", "--help"): 91 | usage() 92 | sys.exit() 93 | elif o in ("-d", "--database"): 94 | if(a and os.path.isfile(a)): 95 | CFG.set('maelstrom', 'database', os.path.abspath(a)) 96 | else: 97 | usage() 98 | sys.exit() 99 | apply(cleanNames,args) 100 | 101 | if __name__ == '__main__': 102 | if(platform.machine()=='i686'): 103 | psyco.full() 104 | # init the app 105 | init() 106 | sys.exit(0) 107 | -------------------------------------------------------------------------------- /www/details.php: -------------------------------------------------------------------------------- 1 | */ 4 | include_once("maelstrom.php"); 5 | 6 | if(isset($_GET['c'])) { 7 | $c=$_GET['c']; 8 | } else { 9 | die; 10 | } 11 | 12 | ?> 13 | 14 | 39 | 40 | 41 | 42 | 43 | 44 | 193 | 194 | 195 |
196 | 200 | 201 |
202 | 203 | 204 | 205 | -------------------------------------------------------------------------------- /utils/index.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | Maelstrom - visualizing email contacts 5 | Copyright© 2008-2009 Stefan Marsiske 6 | 7 | This program is free software: you can redistribute it and/or modify 8 | it under the terms of the GNU General Public License as published by 9 | the Free Software Foundation, either version 3 of the License, or 10 | (at your option) any later version. 11 | 12 | This program is distributed in the hope that it will be useful, 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 15 | GNU General Public License for more details. 16 | 17 | You should have received a copy of the GNU General Public License 18 | along with this program. If not, see . 19 | 20 | 21 | BUGS: the mbox parse chockes on messages that have a line starting with From 22 | in the body. 23 | """ 24 | 25 | import mailbox, sys, os, psyco, datetime, email, getopt 26 | from sqlobject import * 27 | from email.utils import getaddresses, parsedate 28 | from lib.objects import * 29 | from lib.utils import decode_header 30 | from email.feedparser import FeedParser 31 | 32 | SUPPORTEDFORMATS=['mbox', 'cyrus'] 33 | PERSONMAPFILE='db/persons.map' 34 | personmap={} 35 | config={} 36 | 37 | def usage(): 38 | print "usage: %s -d \n" % (sys.argv[0]) 39 | print "\t-h This Help" 40 | print "\t-d verbose message level" 41 | 42 | class eMessage(email.Message.Message): 43 | """Message with mailbox-format-specific properties.""" 44 | 45 | def __init__(self, message=None): 46 | """Initialize a Message instance.""" 47 | feedparser = FeedParser(email.message.Message) 48 | feedparser._set_headersonly() 49 | data = message.read(4096) 50 | feedparser.feed(data) 51 | self._become_message(feedparser.close()) 52 | 53 | def _become_message(self, message): 54 | """Assume the non-format-specific state of message.""" 55 | for name in ('_headers', '_unixfrom', '_payload', '_charset', 56 | 'preamble', 'epilogue', 'defects', '_default_type'): 57 | self.__dict__[name] = message.__dict__[name] 58 | 59 | def fetchEmail(mail,owner=None): 60 | if(not mail): 61 | return 62 | 63 | username='' 64 | mailserver='' 65 | parts=mail.split("@") 66 | if(len(parts)>2): 67 | print mail 68 | print "more than 2 elements to a split emailaddress, bailing out" 69 | sys.exit(1) 70 | elif(len(parts)==2): 71 | mailserver=parts[1].lower() 72 | username=parts[0].lower() 73 | 74 | if(not owner): 75 | owner='' 76 | names=username.split('.') 77 | if(len(names)>1): 78 | ownername=" ".join(map(lambda x: x[0].upper()+x[1:],names)) 79 | owner=fetchPerson(ownername,mail) 80 | else: 81 | owner=fetchPerson('',mail) 82 | 83 | q=Email.select(AND(Email.q.username==username, 84 | Email.q.mailserver==mailserver)) 85 | try: 86 | return q.getOne() 87 | except(SQLObjectNotFound): 88 | return Email(username=username, mailserver=mailserver,owner=owner) 89 | 90 | def fetchPerson(person,mail=None): 91 | if(not person): 92 | return 93 | if(mail and personmap.has_key(mail)): 94 | fullname=personmap[mail] 95 | else: 96 | fullname=decode_header(person).encode("utf-8").strip(" '\"") 97 | q=Person.select(Person.q.fullname==fullname) 98 | try: 99 | return q.getOne() 100 | except(SQLObjectNotFound): 101 | return Person(fullname=fullname) 102 | 103 | def fetchHeader(header): 104 | q=Header.select(Header.q.name==header) 105 | try: 106 | return q.getOne() 107 | except(SQLObjectNotFound): 108 | return Header(name=header.encode("utf-8")) 109 | #print "header",h 110 | except(SQLObjectIntegrityError): 111 | print "oops. database headers probably contains multiple entries for", header 112 | sys.exit(1) 113 | 114 | def parseMbox(file): 115 | for message in mailbox.mbox(file): 116 | msg=parseMessage(message,file) 117 | if(msg): 118 | parseContacts(message,msg) 119 | parseHeaders(message,msg) 120 | #TODO: mailindexer 121 | # parseBody(message,msg) 122 | 123 | def parseMessage(message,file): 124 | # TODO refactor into own fun: create message 125 | if(config.decoder=="mbox"): 126 | unixfrom=message.get_from().split(" ") 127 | if message['date']: 128 | t=parsedate(message['date']) 129 | elif(config.decoder=="mbox"): 130 | t=parsedate(" ".join(unixfrom[1:])) 131 | try: 132 | timestamp=datetime.datetime(*t[:6]) 133 | except: 134 | # pass this message with malformed header 135 | return None 136 | 137 | # fetch sender 138 | senders=getaddresses(message.get_all('from',[])) 139 | if len(senders): 140 | p=fetchPerson(decode_header(senders[0][0]).encode("utf-8"),senders[0][1]) 141 | e=fetchEmail(senders[0][1],p) 142 | elif(config.decoder=="mbox"): 143 | e=fetchEmail(unixfrom[0]) 144 | #print "msg",msg 145 | return Message(delivered=timestamp,messageid=message['message-id'],sender=e,path=file) 146 | 147 | def parseContacts(message,msg): 148 | for field in ["to","cc","resent-to","resent-cc"]: 149 | for address in getaddresses(message.get_all(field, [])): 150 | # fetch person 151 | p=fetchPerson(address[0],address[1]) 152 | e=fetchEmail(address[1],p) 153 | # fetch header 154 | h=fetchHeader(field) 155 | Role(email=e,msg=msg,header=h) 156 | #print "role",role 157 | del message[field] 158 | 159 | def parseHeaders(message,msg): 160 | for header in map(str.lower,message.keys()): 161 | h=fetchHeader(header) 162 | for value in message.get_all(header, []): 163 | value=decode_header(value).encode("utf-8") 164 | hv=HeaderValue(value=value,msg=msg,header=h) 165 | 166 | def parseBody(message,msg): 167 | for part in message.walk(): 168 | if part.get_content_maintype() == 'multipart': 169 | continue 170 | print part.get_content_type(), decode_header(part.get_filename('')).encode("utf-8") 171 | # TODO: if used as mailindexer 172 | # store payloads, possibly with hashed filenames 173 | # feed them to xapian 174 | # print part.get_payload(decode=True) 175 | # # Applications should really sanitize the given filename so that an 176 | # # email message can't be used to overwrite important files 177 | # filename = part.get_filename() 178 | # if not filename: 179 | # ext = mimetypes.guess_extension(part.get_content_type()) 180 | # if not ext: 181 | # # Use a generic bag-of-bits extension 182 | # ext = '.bin' 183 | # filename = 'part-%03d%s' % (counter, ext) 184 | # counter += 1 185 | # fp = open(os.path.join(opts.directory, filename), 'wb') 186 | # fp.write(part.get_payload(decode=True)) 187 | # fp.close() 188 | 189 | def main(): 190 | # load email to person mappings 191 | if(os.path.exists(PERSONMAPFILE)): 192 | fp=open(PERSONMAPFILE,'r') 193 | while(fp): 194 | line=fp.readline() 195 | if not line: 196 | break 197 | (email,name)=line.split(" ",1) 198 | personmap[email]=name.strip() 199 | for file in sys.argv[1:]: 200 | print "parsing message(s):", file 201 | if(config.decoder=="mbox"): 202 | parseMbox(file) 203 | elif(config.decoder=="cyrus"): 204 | message=eMessage(open(file)) 205 | msg=parseMessage(message,file) 206 | if(msg): 207 | parseContacts(message,msg) 208 | parseHeaders(message,msg) 209 | 210 | if __name__=='__main__': 211 | try: 212 | opts, args = getopt.gnu_getopt(sys.argv[1:], 213 | "hd:", 214 | ["help", 215 | "decoder="]) 216 | except getopt.GetoptError: 217 | usage() 218 | sys.exit(2) 219 | 220 | for o, a in opts: 221 | if o in ("-h", "--help"): 222 | usage() 223 | sys.exit() 224 | elif o in ("-d", "--decoder"): 225 | if(a and a in SUPPORTEDFORMATS): 226 | config.decoder = a 227 | else: 228 | usage() 229 | sys.exit() 230 | Header.createTable(ifNotExists=True) 231 | HeaderValue.createTable(ifNotExists=True) 232 | Person.createTable(ifNotExists=True) 233 | Role.createTable(ifNotExists=True) 234 | Email.createTable(ifNotExists=True) 235 | Message.createTable(ifNotExists=True) 236 | psyco.full() 237 | sys.exit(main()) 238 | -------------------------------------------------------------------------------- /utils/getgraph: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | """ 3 | Maelstrom - visualizing email contacts 4 | Copyright(c) 2008-2009 Stefan Marsiske 5 | 6 | This program is free software: you can redistribute it and/or modify 7 | it under the terms of the GNU General Public License as published by 8 | the Free Software Foundation, either version 3 of the License, or 9 | (at your option) any later version. 10 | 11 | This program is distributed in the hope that it will be useful, 12 | but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | GNU General Public License for more details. 15 | 16 | You should have received a copy of the GNU General Public License 17 | along with this program. If not, see . 18 | 19 | extracts a directed graph from the database. 20 | edges are the messages, 21 | nodes are the persons 22 | edges have a type to/cc and a weight (number of mails) 23 | """ 24 | 25 | import sys 26 | import os 27 | import getopt 28 | import platform 29 | if(platform.machine()=='i686'): 30 | import psyco 31 | 32 | from lib.objects import Role 33 | from lib.utils import Obj, UnicodeWriter, counter, CFG 34 | 35 | # ID generators for nodes and eges 36 | NodeIdGenerator = iter(counter(0)) 37 | EdgeIdGenerator = iter(counter(0)) 38 | 39 | def usage(): 40 | """Prints out the --help""" 41 | print "usage: %s" % (sys.argv[0]) 42 | print "\t-h This Help" 43 | print "\t-s|--stats Display stats" 44 | print "\t-f |--format== [dot,log,csv,graphxml] Output format." 45 | print "\t-f |--format== [dot,log,csv] Output format." 46 | 47 | class Node(Obj): 48 | """ 49 | Simple graph node, that keeps track of the edges by 50 | - direction, 51 | - time and 52 | - type/timestamp 53 | """ 54 | def __init__(self, name): 55 | self.__dict__['id'] = "n"+str(NodeIdGenerator.next()) 56 | self.__dict__['name'] = name 57 | self.__dict__['srcTypeStamps'] = [] 58 | self.__dict__['dstTypeStamps'] = [] 59 | 60 | def __str__(self): 61 | return "%s\t(Sender:%d, Receiver:%d)\n" % (self.name, 62 | len(self.srcTypeStamps), 63 | len(self.dstTypeStamps)) 64 | 65 | def incsrcweight(self, mode, date): 66 | """ 67 | add an incomming edge to the node 68 | """ 69 | self.__dict__['srcTypeStamps'].append((date, mode)) 70 | 71 | def incdstweight(self, mode, date): 72 | """ 73 | add an outgoing edge to the node 74 | """ 75 | self.__dict__['dstTypeStamps'].append((date, mode)) 76 | 77 | class Edge(Obj): 78 | """ 79 | Simple graph edge, that keeps track of the nodes by 80 | - type/timestamp 81 | """ 82 | def __init__(self, sender, receiver): 83 | self.__dict__['id'] = "e"+str(EdgeIdGenerator.next()) 84 | self.__dict__['sender'] = sender 85 | self.__dict__['to'] = receiver 86 | self.__dict__['typestamps'] = [] 87 | 88 | def __str__(self): 89 | return "%s -> %s W:%d\n" % (self.sender, 90 | self.to, 91 | len(self.typestamps)) 92 | def incweight(self, mode, date): 93 | """ 94 | adjust the weight of the edge 95 | """ 96 | self.__dict__['typestamps'].append((date, mode)) 97 | 98 | class Graph(Obj): 99 | """ 100 | graph object: implements 101 | - building 102 | - exporting as dot,log,csv 103 | - gathering statistics 104 | """ 105 | def __init__(self): 106 | self.__dict__['nodes'] = {} 107 | self.__dict__['edges'] = {} 108 | self.__dict__['nodemap'] = {} 109 | 110 | def __str__(self): 111 | return "Nodes: %s\nEdges: %s\n" % (self.nodes, self.edges) 112 | 113 | def getNode(self, name): 114 | res=None 115 | if(not name in self.nodes.keys()): 116 | res = Node(name) 117 | self.nodes[name] = res 118 | self.nodemap[name] = res.id 119 | else: 120 | res = self.nodes[name] 121 | return res 122 | 123 | def addedge(self, sender, mode, to, date): 124 | """adds an edge to the graph, creating non-existing nodes as 125 | necessary, adjusting weights at each object properly.""" 126 | src = edge = dst = None 127 | # create nodes if not yet seen 128 | src=self.getNode(sender) 129 | dst=self.getNode(to) 130 | 131 | # create edge if a new one is found 132 | if(not (sender, to) in self.edges.keys()): 133 | edge = Edge(sender, to) 134 | self.edges[(sender, to)] = edge 135 | else: 136 | edge = self.edges[(sender, to)] 137 | 138 | # adjust weight on edge 139 | edge.incweight(mode, date) 140 | 141 | # adjust weight on nodes 142 | src.incsrcweight(mode, date) 143 | dst.incdstweight(mode, date) 144 | 145 | def stats(self): 146 | nodes = self.__dict__['nodes'].values() 147 | nodes.sort(lambda x, y: cmp(len(y.srcTypeStamps)+len(y.dstTypeStamps), 148 | len(x.srcTypeStamps)+len(x.dstTypeStamps))) 149 | print "Top Overall\n", nodes[:10] 150 | 151 | # print out top senders 152 | nodes = self.__dict__['nodes'].values() 153 | nodes.sort(lambda x, y: cmp(len(y.srcTypeStamps), 154 | len(x.srcTypeStamps))) 155 | print "Top Senders\n", nodes[:10] 156 | 157 | # print out top recipients 158 | nodes = self.__dict__['nodes'].values() 159 | nodes.sort(lambda x, y: cmp(len(y.dstTypeStamps), 160 | len(x.dstTypeStamps))) 161 | print "Top Receivers\n", nodes[:10] 162 | 163 | # print out top edges 164 | edges = self.__dict__['edges'].values() 165 | edges.sort(lambda x, y: cmp(len(y.typestamps), len(x.typestamps))) 166 | print "Top Overall edges\n", edges[:10] 167 | 168 | if(CFG.get('maelstrom','egg')): 169 | edges = filter(lambda x: ( 170 | x.sender!=CFG.get('maelstrom','egger')), edges) 171 | edges.sort(lambda x, y: cmp(len(y.typestamps), len(x.typestamps))) 172 | print "Top Overall eggs\n", edges[:10] 173 | 174 | def dot(self): 175 | """ 176 | outputs the graph in graphviz dot language 177 | """ 178 | result = "digraph G {\noverlap = false;\nsplines=true;\n" 179 | result += reduce(lambda y, x: y+'%s [ label="%s"];\n' % 180 | (self.nodes[x.name].id, x.name), 181 | self.nodes.values(),"") 182 | result += reduce(lambda y, x: y+'"%s" -> "%s" [ weight="%d" ];\n' % 183 | (self.nodes[x.sender].id, 184 | self.nodes[x.to].id, 185 | len(x.typestamps)), 186 | self.edges.values(),"") 187 | result += "}" 188 | return result 189 | 190 | def getEdges(self,name,dir=3): 191 | """ returns a list of edges related to the node designated by 192 | name. Dir is a bitmask, bit1 incomming, bit2 outgoing, 193 | default=3 both directions""" 194 | 195 | return filter(lambda x: 196 | ((dir & 1) and x.to==name) or 197 | ((dir & 2) and x.sender==name), 198 | self.edges.values()) 199 | 200 | class PersonMap: 201 | """ 202 | maps names or email addresses to other names specified in a configure 203 | """ 204 | def __init__(self, fname): 205 | self.__dict__['personmap'] = {} 206 | if(os.path.exists(fname)): 207 | fp = open(fname,'r') 208 | while(fp): 209 | line = fp.readline() 210 | if not line: 211 | break 212 | (email, name) = line.split(" ", 1) 213 | self.__dict__['personmap'][email] = name.strip() 214 | fp.close() 215 | 216 | def __getitem__(self, name): 217 | if(name and self.__dict__['personmap'].has_key(name)): 218 | return self.__dict__['personmap'][name] 219 | else: 220 | return name 221 | 222 | def init(): 223 | """ 224 | initializes the configuration from the config files, then sets the 225 | command line parameters and finally passes control to the main 226 | processing function 227 | """ 228 | try: 229 | opts, args = getopt.gnu_getopt( 230 | sys.argv[1:], "hesf:d:", 231 | ["help", "egg", "stats", "format=", "database="]) 232 | except getopt.GetoptError: 233 | usage() 234 | sys.exit(2) 235 | for o, a in opts: 236 | if o in ("-h", "--help"): 237 | usage() 238 | sys.exit() 239 | elif o in ("-d", "--database"): 240 | if(a and os.path.isfile(a)): 241 | CFG.set('maelstrom', 'database', os.path.abspath(a)) 242 | elif o in ("-e", "--egg"): 243 | CFG.set('maelstrom', 'egg', False) 244 | elif o in ("-s", "--stats"): 245 | CFG.set('maelstrom', 'stats', True) 246 | elif o in ("-f", "--format"): 247 | if(a and a in ("dot", "log", "graphxml", "csv")): 248 | CFG.set('maelstrom', 'format', a) 249 | else: 250 | usage() 251 | sys.exit() 252 | 253 | def process(): 254 | graph = Graph() 255 | personmap = PersonMap( 256 | os.path.abspath(CFG.get('maelstrom','personmapfile'))) 257 | csvcoder = None 258 | if(CFG.get('maelstrom','format') == "csv"): 259 | csvcoder = UnicodeWriter(sys.stdout) 260 | 261 | q = Role.select() 262 | for edge in q: 263 | # try to get the names of the participants 264 | try: 265 | sender = personmap[edge.msg.sender.getname()] 266 | except AttributeError: 267 | continue 268 | try: 269 | receiver = personmap[edge.email.getname()] 270 | except AttributeError: 271 | continue 272 | # add edge to graph 273 | graph.addedge(sender, 274 | edge.header.name, 275 | receiver, 276 | edge.msg.delivered) 277 | 278 | # print out edge in log/csv format if necessary 279 | if(CFG.get('maelstrom','format') == "log"): 280 | print edge.msg.delivered, sender, edge.header.name, receiver 281 | elif(CFG.get('maelstrom','format') == "csv"): 282 | csvcoder.writerow(map(lambda x: str(x), 283 | (edge.msg.delivered, 284 | sender, 285 | edge.header.name, 286 | receiver))) 287 | print graph.getEdges(CFG.get('maelstrom','testuser')) 288 | #print graph 289 | if(CFG.get('maelstrom','format') == "dot"): 290 | print graph.dot() 291 | if(CFG.get('maelstrom','stats')): 292 | print graph.stats() 293 | 294 | if __name__ == '__main__': 295 | if(platform.machine()=='i686'): 296 | psyco.full() 297 | # init the app 298 | init() 299 | # call the main routine for processing the data 300 | process() 301 | sys.exit(0) 302 | -------------------------------------------------------------------------------- /www/maelstrom.php: -------------------------------------------------------------------------------- 1 | */ 4 | 5 | $MAILBOXOWNER=""; //IMPORTANT CONFIGURE THESE!!!! 6 | $dburl='sqlite:'.$_SERVER['DOCUMENT_ROOT'].'/maelstrom/db/messages.db'; 7 | try { 8 | $db= new PDO($dburl); 9 | } 10 | catch( PDOException $exception ){ 11 | die($exception->getMessage()); 12 | } 13 | 14 | list($start, $end)=mailTimeFrame($db); 15 | if(isset($_GET['start'])) { 16 | $start=$_GET['start']; 17 | } 18 | if(isset($_GET['end'])) { 19 | $end=$_GET['end']; 20 | } 21 | 22 | // get all mails from person 23 | 24 | function contactMails($db) { 25 | global $MAILBOXOWNER; 26 | global $start, $end; 27 | 28 | if(isset($_GET['c'])) { 29 | $person=$_GET['c']; 30 | } else { 31 | $person=$MAILBOXOWNER; 32 | } 33 | 34 | $q="select count(message.id) as count, 35 | date(message.delivered) as delivered 36 | from message, role, email, person 37 | where message.id=role.msg_id and 38 | role.email_id=email.id and 39 | email.owner_id==person.id and 40 | person.fullname=='$person' 41 | group by delivered 42 | order by delivered"; //and 43 | //date(message.delivered)>='$start' AND 44 | //date(message.delivered)<'$end'"; 45 | // TODO add window handling if needed 46 | 47 | foreach ($db->query($q) as $row) { 48 | $results[]=array('date' => $row['delivered'], 49 | 'count' => $row['count']); 50 | } 51 | return ($results); 52 | } 53 | 54 | // get all contacts with weights for person 55 | 56 | function secondContacts($db) { 57 | global $start, $end; 58 | if(isset($_GET['c'])) { 59 | $person=$_GET['c']; 60 | } else { 61 | $person=$MAILBOXOWNER; 62 | } 63 | 64 | // TODO add window handling if needed, should be enough if the subquery 65 | // return messages in the interval 66 | //and date(message.delivered)>='$start' AND 67 | //date(message.delivered)<'$end'"; 68 | $q="select person.fullname as contact, 69 | count(person.id) as count, 70 | date(message.delivered) as date 71 | from message, role, email, person 72 | where message.id in (select message.id 73 | from message, role, email, person 74 | where message.id==role.msg_id and 75 | role.email_id==email.id and 76 | email.owner_id==person.id and 77 | person.fullname like '$person') and 78 | message.id==role.msg_id and 79 | role.email_id==email.id and 80 | email.owner_id==person.id and 81 | person.fullname not like '$person' 82 | group by contact 83 | order by date;"; 84 | $results=array(); 85 | $curdate="00-00-00"; 86 | foreach ($db->query($q) as $row) { 87 | //$results[]=array($row['contact'],$row['count'],$row['date']); 88 | //print_r($row); 89 | //print "
"; 90 | $date=$row['date']; 91 | if($date>$curdate) { 92 | // row is a new day 93 | if(isset($res)) { 94 | // store the list of contact volumes in result vector 95 | $results[]=array($curdate, $res); 96 | } 97 | // create a new list of contact volumes 98 | $res=array(array($row['contact'], $row['count'])); 99 | // set curdate to the currently created new day 100 | $curdate=$date; 101 | } elseif($date==$curdate) { 102 | // store the contact in the current days volume list 103 | $res[]=array($row['contact'], $row['count']); 104 | } else { 105 | print "curdate $curdate"; 106 | print "date $date"; 107 | print_r($row); 108 | } 109 | } 110 | // append the last day 111 | if($res) { 112 | $results[]=array($curdate, $res); 113 | } 114 | return ($results); 115 | } 116 | 117 | function mailTimeFrame($db) { 118 | $q='SELECT max(delivered) AS max, min(delivered) as min FROM message'; 119 | $result=$db->query($q)->fetch(); 120 | $start=split(" ",$result['min']); 121 | $start=$start[0]; 122 | $end=split(" ",$result['max']); 123 | $end=$end[0]; 124 | return array($start,$end); 125 | } 126 | 127 | function contactTimeCloud($db) { 128 | global $start, $end; 129 | global $MAILBOXOWNER; 130 | 131 | $q="SELECT person.fullname as contact, 132 | count(message.id) as count, 133 | date(message.delivered) as date 134 | FROM person, email, role, message 135 | WHERE role.email_id==email.id AND 136 | email.owner_id==person.id AND 137 | role.msg_id==message.id AND 138 | person.fullname!='$MAILBOXOWNER' AND 139 | date(message.delivered)>='$start' AND 140 | date(message.delivered)<'$end' 141 | GROUP BY date(delivered), person.fullname 142 | ORDER BY date(delivered)"; 143 | $results=array(); 144 | $curdate="00-00-00"; 145 | foreach ($db->query($q) as $row) { 146 | //print_r($row); 147 | //print "
"; 148 | $date=$row['date']; 149 | if($date>$curdate) { 150 | // row is a new day 151 | if(isset($res)) { 152 | // store the list of contact volumes in result vector 153 | $results[]=array($curdate, $res); 154 | } 155 | // create a new list of contact volumes 156 | $res=array(array($row['contact'], $row['count'])); 157 | // set curdate to the currently created new day 158 | $curdate=$date; 159 | } elseif($date==$curdate) { 160 | // store the contact in the current days volume list 161 | $res[]=array($row['contact'], $row['count']); 162 | } else { 163 | print "curdate $curdate"; 164 | print "date $date"; 165 | print_r($row); 166 | } 167 | } 168 | // append the last day 169 | if($res) { 170 | $results[]=array($curdate, $res); 171 | } 172 | return ($results); 173 | } 174 | 175 | function contactOrgs($db) { 176 | global $start, $end; 177 | 178 | if(isset($_GET['c'])) { 179 | $user=$_GET['c']; 180 | } else { 181 | $user=$MAILBOXOWNER; 182 | } 183 | 184 | $q="select mailserver as org, 185 | count(role.id) as count, 186 | date(message.delivered) as date 187 | from email, person, role, message 188 | where role.email_id==email.id and 189 | email.owner_id==person.id and 190 | role.msg_id==message.id and 191 | fullname='$user' 192 | group by org, delivered 193 | order by delivered;"; 194 | $results=array(); 195 | $curdate="00-00-00"; 196 | foreach ($db->query($q) as $row) { 197 | //print_r($row); 198 | //print "
"; 199 | $date=$row['date']; 200 | if($date>$curdate) { 201 | // row is a new day 202 | if(isset($res)) { 203 | // store the list of contact volumes in result vector 204 | $results[]=array($curdate, $res); 205 | } 206 | // create a new list of contact volumes 207 | $res=array(array($row['org'], $row['count'])); 208 | // set curdate to the currently created new day 209 | $curdate=$date; 210 | } elseif($date==$curdate) { 211 | // store the contact in the current days volume list 212 | $res[]=array($row['org'], $row['count']); 213 | } else { 214 | header("Content-type: text/plain"); 215 | print "curdate $curdate"; 216 | print "date $date"; 217 | print_r($row); 218 | } 219 | } 220 | // append the last day 221 | if($res) { 222 | $results[]=array($curdate, $res); 223 | } 224 | return ($results); 225 | } 226 | 227 | function orgContacts($db) { 228 | global $start, $end; 229 | 230 | if(!isset($_GET['o'])) { 231 | die; 232 | } 233 | $org=$_GET['o']; 234 | $q="select person.fullname as contact, 235 | count(person.id) as count, 236 | date(message.delivered) as date 237 | from message, role, email, person 238 | where mailserver like '$org' and 239 | message.id==role.msg_id and 240 | role.email_id==email.id and 241 | email.owner_id==person.id 242 | group by contact 243 | order by date;"; 244 | $results=array(); 245 | $curdate="00-00-00"; 246 | foreach ($db->query($q) as $row) { 247 | //$results[]=array($row['contact'],$row['count'],$row['date']); 248 | //print_r($row); 249 | //print "
"; 250 | $date=$row['date']; 251 | if($date>$curdate) { 252 | // row is a new day 253 | if(isset($res)) { 254 | // store the list of contact volumes in result vector 255 | $results[]=array($curdate, $res); 256 | } 257 | // create a new list of contact volumes 258 | $res=array(array($row['contact'], $row['count'])); 259 | // set curdate to the currently created new day 260 | $curdate=$date; 261 | } elseif($date==$curdate) { 262 | // store the contact in the current days volume list 263 | $res[]=array($row['contact'], $row['count']); 264 | } else { 265 | print "curdate $curdate"; 266 | print "date $date"; 267 | print_r($row); 268 | } 269 | } 270 | // append the last day 271 | if($res) { 272 | $results[]=array($curdate, $res); 273 | } 274 | return ($results); 275 | } 276 | 277 | function mailFrequency($db) { 278 | global $start, $end; 279 | 280 | $q="SELECT date(delivered) as delivered, 281 | count(id) as count 282 | FROM message 283 | WHERE delivered>='$start' AND 284 | delivered<'$end' 285 | GROUP BY date(delivered) 286 | ORDER BY date(delivered)"; 287 | 288 | foreach ($db->query($q) as $row) { 289 | $results[]=array('count' => $row['count'], 290 | 'date' => $row['delivered']); 291 | } 292 | return ($results); 293 | } 294 | 295 | function getEdgeTotalWeight($db) { 296 | global $_GET; 297 | global $start, $end; 298 | if(isset($_GET['c1'])) { 299 | $c1=$_GET['c1']; 300 | } else { 301 | die; 302 | } 303 | if(isset($_GET['c2'])) { 304 | $c2=$_GET['c2']; 305 | } else { 306 | die; 307 | } 308 | $q="SELECT date(delivered) as delivered, 309 | count(message.id) as count 310 | FROM message, 311 | email as se, 312 | person as sp, 313 | role, 314 | email as re, 315 | person as rp 316 | WHERE ((sp.fullname==:c1 and rp.fullname==:c2) or 317 | (sp.fullname==:c2 and rp.fullname==:c1 )) and 318 | date(delivered)>=:start AND 319 | date(delivered)<:end and 320 | 321 | se.id==message.sender_id and 322 | sp.id==se.owner_id and 323 | role.msg_id==message.id and 324 | re.id==role.email_id and 325 | rp.id==re.owner_id 326 | GROUP BY date(delivered) 327 | ORDER BY date(delivered)"; 328 | 329 | $query = $db->prepare($q); 330 | $query->execute(array(":c1" => $c1, 331 | ":c2" => $c2, 332 | ":start" => $start, 333 | ":end" => $end)); 334 | for($i=0; $row = $query->fetch(); $i++){ 335 | $results[]=array('count' => $row['count'], 336 | 'date' => $row['delivered']); 337 | } 338 | return ($results); 339 | } 340 | 341 | function getEdgeWeights($db) { 342 | global $_GET; 343 | global $start, $end; 344 | if(isset($_GET['c1'])) { 345 | $c1=$_GET['c1']; 346 | } else { 347 | die; 348 | } 349 | if(isset($_GET['c2'])) { 350 | $c2=$_GET['c2']; 351 | } else { 352 | die; 353 | } 354 | $q="SELECT header.name as type, 355 | date(delivered) as delivered, 356 | count(message.id) as count 357 | FROM message, 358 | email as se, 359 | person as sp, 360 | header, 361 | role, 362 | email as re, 363 | person as rp 364 | WHERE ((sp.fullname==:c1 and rp.fullname==:c2) or 365 | (sp.fullname==:c2 and rp.fullname==:c1 )) and 366 | date(delivered)>=:start AND 367 | date(delivered)<:end and 368 | 369 | se.id==message.sender_id and 370 | sp.id==se.owner_id and 371 | role.header_id==header.id and 372 | role.msg_id==message.id and 373 | re.id==role.email_id and 374 | rp.id==re.owner_id 375 | GROUP BY type, date(delivered) 376 | ORDER BY date(delivered)"; 377 | 378 | $query = $db->prepare($q); 379 | if (!$query) { 380 | echo "\nPDO::errorInfo():\n"; 381 | print_r($db->errorInfo()); 382 | } 383 | $query->execute(array(":c1" => $c1, 384 | ":c2" => $c2, 385 | ":start" => $start, 386 | ":end" => $end)); 387 | // to/cc is enough since the query works both ways a-b&b-a this 388 | // means we can ignore the from field 389 | $results=array('to' => array(), 'cc' => array()); 390 | for($i=0; $row = $query->fetch(); $i++){ 391 | $results[$row['type']][]=array('date' => $row['delivered'], 392 | 'count' => $row['count']); 393 | } 394 | 395 | return ($results); 396 | } 397 | 398 | if(isset($_GET['op'])) { 399 | try { 400 | $db= new PDO($dburl); 401 | } 402 | catch( PDOException $exception ){ 403 | die($exception->getMessage()); 404 | } 405 | 406 | if($_GET['op']=="contactTimeCloud") { 407 | //header("Content-type: text/plain"); 408 | print json_encode(contactTimeCloud($db)); 409 | } 410 | elseif($_GET['op']=="mailFrequency") { 411 | //header("Content-type: text/plain"); 412 | print json_encode(mailFrequency($db)); 413 | } 414 | elseif($_GET['op']=="contactMails") { 415 | //header("Content-type: text/plain"); 416 | print json_encode(contactMails($db)); 417 | } 418 | elseif($_GET['op']=="secondContacts") { 419 | //header("Content-type: text/plain"); 420 | print json_encode(secondContacts($db)); 421 | } 422 | elseif($_GET['op']=="contactOrgs") { 423 | //header("Content-type: text/plain"); 424 | print json_encode(contactOrgs($db)); 425 | } 426 | elseif($_GET['op']=="orgContacts") { 427 | //header("Content-type: text/plain"); 428 | print json_encode(orgContacts($db)); 429 | } 430 | elseif($_GET['op']=="getEdgeTotalWeight") { 431 | //header("Content-type: text/plain"); 432 | print json_encode(getEdgeTotalWeight($db)); 433 | } 434 | elseif($_GET['op']=="getEdgeWeights") { 435 | //header("Content-type: text/plain"); 436 | print json_encode(getEdgeWeights($db)); 437 | } 438 | } 439 | 440 | function getEdges() { 441 | global $db,$_GET; 442 | global $start, $end; 443 | if(isset($_GET['c'])) { 444 | $c=$_GET['c']; 445 | } else { 446 | die; 447 | } 448 | 449 | $q="select person.fullname as contact, 450 | count(person.fullname) as weight 451 | from person as p, 452 | email, 453 | message, 454 | role, 455 | email as rec, 456 | person 457 | where p.fullname==:c and 458 | date(delivered)>=:start AND 459 | date(delivered)<:end and 460 | p.id==rec.owner_id and 461 | rec.id==role.email_id and 462 | role.msg_id==message.id and 463 | email.id==message.sender_id and 464 | person.id==email.owner_id 465 | group by contact 466 | having count(person.fullname)>1 467 | order by weight desc, contact;"; 468 | $cache=array(); 469 | $query = $db->prepare($q); 470 | $query->execute(array(":c" => $c, 471 | ":start" => $start, 472 | ":end" => $end)); 473 | for($i=0; $row = $query->fetch(); $i++){ 474 | if(array_key_exists($row['contact'],$cache)){ 475 | $cache[$row['contact']]=array('from' => $row['weight']); 476 | $cache['total']+=$row['weight']; 477 | } else { 478 | $cache[$row['contact']]=array('from'=>$row['weight'], 479 | 'total'=>$row['weight']); 480 | } 481 | } 482 | $q="select header.name as type, 483 | p.fullname as contact, 484 | count(person.fullname) as weight 485 | from person, 486 | email, 487 | message, 488 | header, 489 | role, 490 | email as rec, 491 | person as p 492 | where person.fullname==:c and 493 | date(delivered)>=:start AND 494 | date(delivered)<:end and 495 | email.owner_id==person.id and 496 | message.sender_id==email.id and 497 | message.id==role.msg_id and 498 | role.email_id==rec.id and 499 | role.header_id==header.id and 500 | rec.owner_id==p.id 501 | group by contact, type 502 | having count(person.fullname)>1 503 | order by weight desc, contact 504 | ;"; 505 | 506 | $query = $db->prepare($q); 507 | $query->execute(array(":c" => $c, 508 | ":start" => $start, 509 | ":end" => $end)); 510 | for($i=0; $row = $query->fetch(); $i++){ 511 | if(array_key_exists($row['contact'],$cache)){ 512 | $cache[$row['contact']][$row['type']]=$row['weight']; 513 | $cache[$row['contact']]['total']+=$row['weight']; 514 | } else { 515 | $cache[$row['contact']]=array($row['type'] => $row['weight'], 'total'=>$row['weight']); 516 | } 517 | } 518 | 519 | uasort($cache,'cmpComposite'); 520 | foreach($cache as $key => $item) { 521 | ?> 522 |
523 | 524 | $weight) { 525 | if(!strcmp($type,'total')) continue; 526 | ?> 527 |
528 | 529 |
530 | 531 |
532 |
533 |
534 |
535 |
536 |
537 |
Sparkline Loading...
538 |
539 | 540 | 541 |
542 |
543 |
544 | $b['total']) ? -1 : 1; 553 | } 554 | 555 | ?> 556 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published by 637 | the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | --------------------------------------------------------------------------------