From 16015f8559fccb62ab419166daaaac2f86eafedd Mon Sep 17 00:00:00 2001 From: Mario Fetka Date: Thu, 16 Jul 2026 14:26:58 +0200 Subject: [PATCH] Modernize Bongo 0.7.0 foundation --- CMakeLists.txt | 39 +- README | 24 +- ROADMAP.md | 127 + cmake/Directories.cmake | 4 +- cmake/Distribution.cmake | 24 +- cmake/EmbedFile.cmake | 18 + cmake/FindOpenDKIM.cmake | 14 + cmake/FindOpenDMARC.cmake | 14 + cmake/FindSPF2.cmake | 14 + cmake/FindSRS2.cmake | 14 + cmake/Requirements.cmake | 56 +- config.h.cmake | 5 +- docs/antispam.md | 40 + include/bongoagent.h | 2 +- include/bongosasl.h | 40 + include/bongosieve.h | 98 + include/bongoutil.h | 26 +- include/connio.h | 6 +- include/memmgr.h | 2 +- include/msgaddr.h | 4 +- include/msgdate.h | 14 +- include/rulesrv.h | 2 + include/streamio.h | 9 +- init/CMakeLists.txt | 23 + init/bongo.service.in | 27 + init/bongo.sysusers.in | 1 + init/bongo.tmpfiles.in | 9 + po/bongo-web/de.po | 105 + po/bongo-web/fr.po | 102 +- po/bongo-web/pt_BR.po | 94 +- src/agents/antispam/antispam.c | 3 +- src/agents/antispam/antispam.h | 2 + src/agents/antispam/spamd.c | 28 +- src/agents/avirus/avirus.c | 175 +- src/agents/avirus/avirus.h | 20 +- src/agents/collector/CMakeLists.txt | 32 + src/agents/collector/collector.c | 33 +- src/agents/collector/external-accounts.sql | 70 + src/agents/collector/external_accounts.c | 76 + src/agents/collector/external_accounts.h | 10 + src/agents/collector/provider-presets.json | 144 + src/agents/imap/CMakeLists.txt | 1 + src/agents/imap/copy.c | 6 +- src/agents/imap/event.c | 12 +- src/agents/imap/fetch.c | 458 +-- src/agents/imap/fetch.h | 14 +- src/agents/imap/imapd.c | 174 +- src/agents/imap/imapd.h | 6 +- src/agents/imap/inline.h | 50 +- src/agents/imap/rfc822parse.c | 99 +- src/agents/imap/rfc822parse.h | 14 +- src/agents/imap/search.c | 176 +- src/agents/imap/session.c | 152 +- src/agents/imap/store.c | 12 +- src/agents/imap/store.h | 9 +- src/agents/imap/uid.c | 13 +- src/agents/pop/CMakeLists.txt | 1 + src/agents/pop/pop3.c | 367 +- src/agents/pop/pop3.h | 6 +- src/agents/queue/conf.c | 7 +- src/agents/queue/conf.h | 6 +- src/agents/queue/domain.c | 19 +- src/agents/queue/domain.h | 10 +- src/agents/queue/mime.c | 22 +- src/agents/queue/mime.h | 14 +- src/agents/queue/queue.c | 562 +-- src/agents/queue/queue.h | 4 +- src/agents/queue/queued.c | 230 +- src/agents/queue/queued.h | 12 +- src/agents/rules/CMakeLists.txt | 1 + src/agents/rules/rules.c | 482 ++- src/agents/rules/rules.h | 18 +- src/agents/sieve/CMakeLists.txt | 5 + src/agents/sieve/managesieved.c | 326 ++ src/agents/smtp/CMakeLists.txt | 2 + src/agents/smtp/smtpc.c | 337 +- src/agents/smtp/smtpc.h | 19 +- src/agents/smtp/smtpd.c | 1127 +++++- src/agents/smtp/smtpd.h | 6 +- src/agents/store/command-parsing.c | 11 +- src/agents/store/command-parsing.h | 6 +- src/agents/store/command.c | 39 +- src/agents/store/contacts.c | 10 +- src/agents/store/cookie.c | 33 +- src/agents/store/db.c | 7 +- src/agents/store/guid.c | 2 +- src/agents/store/mail.c | 15 +- src/agents/store/maildir.c | 16 +- src/agents/store/mime.c | 10 +- src/agents/store/object-model.c | 66 +- src/agents/store/object-model.h | 2 +- src/agents/store/query-builder.c | 12 +- src/agents/store/search.c | 4 +- src/agents/store/store.c | 12 +- src/agents/store/stored.c | 13 +- src/agents/store/stored.h | 3 +- .../admin/bongo/admintool/DemoCommands.py | 12 +- src/apps/config/config.c | 222 +- src/apps/config/config.h | 10 +- src/apps/config/config/antispam | 3 + src/apps/config/config/imap | 1 + src/apps/config/config/manager | 3 +- src/apps/config/config/pop3 | 3 +- src/apps/config/config/sieve | 8 + src/apps/config/config/smtp | 33 +- src/apps/config/store.c | 20 +- src/apps/manager/manager.c | 3 + src/apps/storetool/CMakeLists.txt | 4 +- .../bongo/storetool/CalendarCommands.py | 12 +- .../bongo/storetool/ContactCommands.py | 4 +- .../bongo/storetool/ExportMailbox.py | 32 +- .../storetool/bongo/storetool/MailCommands.py | 22 +- src/libs/cal/bongo-cal-instance.c | 2 +- src/libs/cal/bongo-cal-time.c | 11 +- src/libs/connio/connio.c | 152 +- src/libs/connio/conniop.h | 2 +- src/libs/connio/nmap.c | 7 +- src/libs/json/json.c | 12 +- src/libs/json/parse.c | 18 +- src/libs/mailauth/CMakeLists.txt | 48 + src/libs/mailauth/dkim.c | 155 + src/libs/mailauth/dmarc.c | 7 + src/libs/mailauth/mailauth.c | 19 + src/libs/mailauth/mailauth.h | 77 + src/libs/mailauth/spf.c | 91 + src/libs/mailauth/srs.c | 123 + src/libs/mailauth/tests/dkim-test.c | 41 + src/libs/mailauth/tests/spf-test.c | 12 + src/libs/mailauth/tests/srs-test.c | 37 + src/libs/msgapi/auth-backends/sqlite.c | 7 +- src/libs/msgapi/msgaddr.c | 5 +- src/libs/msgapi/msgapi.c | 152 +- src/libs/msgapi/msgauth.c | 2 +- src/libs/msgapi/msgcollector.c | 9 +- src/libs/msgapi/msgdate.c | 94 +- src/libs/msgapi/sqldb.c | 30 +- src/libs/python/bongo/CMakeLists.txt | 4 +- src/libs/python/bongo/Contact.py | 3 +- src/libs/python/bongo/admin/ManagedSlapd.py | 23 +- src/libs/python/bongo/cmdparse.py | 2 +- .../bongo/external/dateutil/__init__.py | 8 - .../python/bongo/external/dateutil/easter.py | 92 - .../python/bongo/external/dateutil/parser.py | 875 ----- .../bongo/external/dateutil/relativedelta.py | 432 --- .../python/bongo/external/dateutil/rrule.py | 1090 ------ src/libs/python/bongo/external/dateutil/tz.py | 924 ----- .../python/bongo/external/dateutil/tzwin.py | 180 - .../external/dateutil/zoneinfo/__init__.py | 80 - .../dateutil/zoneinfo/zoneinfo-2005q.tar.gz | Bin 73514 -> 0 bytes .../python/bongo/external/email/__init__.py | 123 - .../python/bongo/external/email/_parseaddr.py | 479 --- .../python/bongo/external/email/base64mime.py | 184 - .../python/bongo/external/email/charset.py | 398 -- .../python/bongo/external/email/encoders.py | 88 - .../python/bongo/external/email/errors.py | 57 - .../python/bongo/external/email/feedparser.py | 479 --- .../bongo/external/email/filegenerator.py | 203 - .../python/bongo/external/email/generator.py | 352 -- .../python/bongo/external/email/header.py | 502 --- .../python/bongo/external/email/iterators.py | 73 - .../python/bongo/external/email/message.py | 856 ----- .../bongo/external/email/mime/__init__.py | 0 .../bongo/external/email/mime/application.py | 36 - .../python/bongo/external/email/mime/audio.py | 73 - .../python/bongo/external/email/mime/base.py | 26 - .../python/bongo/external/email/mime/image.py | 46 - .../bongo/external/email/mime/message.py | 34 - .../bongo/external/email/mime/multipart.py | 41 - .../bongo/external/email/mime/nonmultipart.py | 26 - .../python/bongo/external/email/mime/text.py | 30 - .../python/bongo/external/email/parser.py | 91 - .../python/bongo/external/email/payloads.py | 154 - .../python/bongo/external/email/quoprimime.py | 336 -- .../bongo/external/email/test/__init__.py | 0 .../external/email/test/data/PyBanner048.gif | Bin 954 -> 0 bytes .../external/email/test/data/audiotest.au | Bin 23493 -> 0 bytes .../bongo/external/email/test/data/msg_01.txt | 19 - .../bongo/external/email/test/data/msg_02.txt | 135 - .../bongo/external/email/test/data/msg_03.txt | 16 - .../bongo/external/email/test/data/msg_04.txt | 37 - .../bongo/external/email/test/data/msg_05.txt | 28 - .../bongo/external/email/test/data/msg_06.txt | 33 - .../bongo/external/email/test/data/msg_07.txt | 83 - .../bongo/external/email/test/data/msg_08.txt | 24 - .../bongo/external/email/test/data/msg_09.txt | 24 - .../bongo/external/email/test/data/msg_10.txt | 32 - .../bongo/external/email/test/data/msg_11.txt | 7 - .../bongo/external/email/test/data/msg_12.txt | 36 - .../external/email/test/data/msg_12a.txt | 38 - .../bongo/external/email/test/data/msg_13.txt | 94 - .../bongo/external/email/test/data/msg_14.txt | 23 - .../bongo/external/email/test/data/msg_15.txt | 52 - .../bongo/external/email/test/data/msg_16.txt | 123 - .../bongo/external/email/test/data/msg_17.txt | 12 - .../bongo/external/email/test/data/msg_19.txt | 43 - .../bongo/external/email/test/data/msg_20.txt | 22 - .../bongo/external/email/test/data/msg_21.txt | 20 - .../bongo/external/email/test/data/msg_22.txt | 46 - .../bongo/external/email/test/data/msg_23.txt | 8 - .../bongo/external/email/test/data/msg_24.txt | 10 - .../bongo/external/email/test/data/msg_25.txt | 117 - .../bongo/external/email/test/data/msg_26.txt | 45 - .../bongo/external/email/test/data/msg_27.txt | 15 - .../bongo/external/email/test/data/msg_28.txt | 25 - .../bongo/external/email/test/data/msg_29.txt | 22 - .../bongo/external/email/test/data/msg_30.txt | 23 - .../bongo/external/email/test/data/msg_31.txt | 15 - .../bongo/external/email/test/data/msg_32.txt | 14 - .../bongo/external/email/test/data/msg_33.txt | 29 - .../bongo/external/email/test/data/msg_34.txt | 19 - .../bongo/external/email/test/data/msg_35.txt | 4 - .../bongo/external/email/test/data/msg_36.txt | 40 - .../bongo/external/email/test/data/msg_37.txt | 22 - .../bongo/external/email/test/data/msg_38.txt | 101 - .../bongo/external/email/test/data/msg_39.txt | 83 - .../bongo/external/email/test/data/msg_40.txt | 10 - .../bongo/external/email/test/data/msg_41.txt | 8 - .../bongo/external/email/test/data/msg_42.txt | 20 - .../bongo/external/email/test/data/msg_43.txt | 217 -- .../bongo/external/email/test/data/msg_44.txt | 35 - .../bongo/external/email/test/test_email.py | 3252 ---------------- .../external/email/test/test_email_codecs.py | 77 - .../email/test/test_email_codecs_renamed.py | 77 - .../external/email/test/test_email_renamed.py | 3255 ----------------- .../external/email/test/test_email_torture.py | 138 - src/libs/python/bongo/external/email/utils.py | 327 -- .../bongo/external/simplejson/__init__.py | 207 -- .../bongo/external/simplejson/decoder.py | 271 -- .../bongo/external/simplejson/encoder.py | 289 -- .../bongo/external/simplejson/jsonfilter.py | 40 - .../bongo/external/simplejson/scanner.py | 63 - .../bongo/external/simpletal/simpleTAL.py | 2 +- .../python/bongo/external/vobject/__init__.py | 86 - .../python/bongo/external/vobject/base.py | 950 ----- .../python/bongo/external/vobject/behavior.py | 143 - .../bongo/external/vobject/icalendar.py | 1661 --------- .../python/bongo/external/vobject/tests.py | 601 --- .../python/bongo/external/vobject/vcard.py | 179 - .../python/bongo/external/vobject/win32tz.py | 156 - src/libs/python/bongo/nmap/CommandStream.py | 2 +- src/libs/python/bongo/store/CommandStream.py | 31 +- src/libs/python/bongo/store/QueueClient.py | 5 +- src/libs/python/bongo/store/StoreClient.py | 10 +- src/libs/python/libbongo/CMakeLists.txt | 21 +- src/libs/python/libbongo/bongoutil.c | 8 +- src/libs/python/libbongo/bootstrap.c | 29 +- src/libs/python/libbongo/cal.c | 33 +- src/libs/python/libbongo/calendar-module.c | 49 + src/libs/python/libbongo/json.c | 196 +- src/libs/python/libbongo/json.h | 13 - src/libs/python/libbongo/libs.c | 31 +- src/libs/python/libbongo/msgapi-defs.c | 28 + src/libs/python/libbongo/msgapi.c | 61 +- src/libs/python/libbongo/pybongo.c | 39 +- src/libs/python/libbongo/pybongo.h | 7 + src/libs/python/libbongo/streamio.c | 49 +- src/libs/python/libbongo/tests.c | 8 +- src/libs/sasl/CMakeLists.txt | 10 + src/libs/sasl/sasl.c | 177 + src/libs/sasl/tests/sasl-test.c | 43 + src/libs/sieve/CMakeLists.txt | 14 + src/libs/sieve/managesieve.c | 134 + src/libs/sieve/sieve.c | 307 ++ src/libs/sieve/store.c | 260 ++ src/libs/sieve/tests/managesieve-test.c | 27 + src/libs/sieve/tests/sieve-test.c | 128 + src/libs/sieve/tests/store-test.c | 66 + src/libs/streamio/bongostream.c | 3 +- src/libs/streamio/codec/big5.h | 2 +- src/libs/streamio/codec/euc_kr.h | 2 +- src/libs/streamio/codec/euc_tw.h | 2 +- src/libs/streamio/codec/gb2312.h | 2 +- src/libs/streamio/codec/html.h | 76 +- src/libs/streamio/codec/hz.h | 2 +- src/libs/streamio/codec/iso2022_cn.h | 5 +- src/libs/streamio/codec/iso2022_jp.h | 2 +- src/libs/streamio/codec/iso2022_kr.h | 2 +- src/libs/streamio/codec/q.h | 3 +- src/libs/streamio/codec/qp.h | 3 +- src/libs/streamio/codec/rfc1522.h | 15 +- src/libs/streamio/codec/rfc2231.h | 14 +- src/libs/streamio/codec/rfc822.h | 2 +- src/libs/streamio/codec/search.h | 12 +- src/libs/streamio/codec/sjis.h | 2 +- src/libs/streamio/streamio.c | 15 +- src/libs/streamio/streamiop.h | 5 +- src/libs/util/bongoagent.c | 17 +- src/libs/util/bongokeyword.c | 10 +- src/libs/util/bongoutil.c | 56 +- src/libs/util/pools.c | 2 +- src/libs/util/stringbuilder.c | 4 +- src/libs/util/utf7mod.c | 12 +- src/libs/xpl/dir.c | 18 +- src/libs/xpl/dns.c | 12 +- src/libs/xpl/hash.c | 38 +- src/www/CMakeLists.txt | 46 + src/www/bongo-web.json | 14 + src/www/bongo-web.py.in | 903 +++++ src/www/bongo_web/__init__.py | 1 + src/www/bongo_web/auth.py | 73 + src/www/bongo_web/calendar_store.py | 202 + src/www/bongo_web/card_store.py | 222 ++ src/www/bongo_web/compose.py | 107 + src/www/bongo_web/dav.py | 281 ++ src/www/bongo_web/filters.py | 133 + src/www/bongo_web/html_mail.py | 71 + src/www/bongo_web/mail_store.py | 199 + src/www/bongo_web/session.py | 89 + src/www/bongo_web/tasks.py | 94 + src/www/static/Bongo-logo.png | Bin 0 -> 5439 bytes src/www/static/agent-control.png | Bin 0 -> 3386 bytes src/www/static/app.css | 82 + src/www/static/app.js | 310 ++ src/www/static/bg-page.png | Bin 0 -> 9911 bytes src/www/static/bg-selected.png | Bin 0 -> 510 bytes src/www/static/calendar-16.png | Bin 0 -> 537 bytes src/www/static/contact-new.png | Bin 0 -> 2457 bytes src/www/static/css/admin.css | 582 +++ src/www/static/css/calendar.css | 1070 ++++++ src/www/static/css/composer.css | 116 + src/www/static/css/contacts.css | 163 + src/www/static/css/conversation.css | 394 ++ src/www/static/css/layout.css | 1225 +++++++ src/www/static/css/log.css | 106 + src/www/static/css/login.css | 92 + src/www/static/css/print.css | 201 + src/www/static/css/summary.css | 139 + src/www/static/css/widgets.css | 568 +++ src/www/static/dragonfly.png | Bin 0 -> 6971 bytes src/www/static/favicon.ico | Bin 0 -> 1406 bytes src/www/static/img/Bongo-logo.png | Bin 0 -> 5439 bytes .../static/img/accessories-text-editor.gif | Bin 0 -> 401 bytes .../static/img/accessories-text-editor.png | Bin 0 -> 439 bytes src/www/static/img/addressbook-16.png | Bin 0 -> 776 bytes src/www/static/img/agent-bongoantispam.png | Bin 0 -> 3882 bytes src/www/static/img/agent-bongoavirus.png | Bin 0 -> 2866 bytes src/www/static/img/agent-bongocalcmd.png | Bin 0 -> 2017 bytes src/www/static/img/agent-bongocollector.png | Bin 0 -> 2017 bytes src/www/static/img/agent-bongoimap.png | Bin 0 -> 3421 bytes src/www/static/img/agent-bongomailprox.png | Bin 0 -> 3165 bytes src/www/static/img/agent-bongopluspack.png | Bin 0 -> 703 bytes src/www/static/img/agent-bongopop3.png | Bin 0 -> 3421 bytes src/www/static/img/agent-bongoqueue.png | Bin 0 -> 2017 bytes src/www/static/img/agent-bongosmtp.png | Bin 0 -> 2338 bytes src/www/static/img/agent-control.png | Bin 0 -> 3386 bytes src/www/static/img/agent-global.png | Bin 0 -> 4014 bytes src/www/static/img/agent-unknown.png | Bin 0 -> 2017 bytes src/www/static/img/airplane-emblem.png | Bin 0 -> 764 bytes src/www/static/img/ajax-loader.gif | Bin 0 -> 1849 bytes src/www/static/img/applications-internet.gif | Bin 0 -> 1067 bytes src/www/static/img/applications-internet.png | Bin 0 -> 924 bytes src/www/static/img/arrow.png | Bin 0 -> 164 bytes src/www/static/img/bg-button-pressed.png | Bin 0 -> 212 bytes src/www/static/img/bg-button-selected.png | Bin 0 -> 227 bytes src/www/static/img/bg-button.png | Bin 0 -> 320 bytes src/www/static/img/bg-header.png | Bin 0 -> 268 bytes src/www/static/img/bg-page.png | Bin 0 -> 9911 bytes src/www/static/img/bg-selected.png | Bin 0 -> 510 bytes src/www/static/img/bg-shine.png | Bin 0 -> 3371 bytes src/www/static/img/bg-side-title-hover.png | Bin 0 -> 233 bytes src/www/static/img/bg-side-title.png | Bin 0 -> 277 bytes src/www/static/img/bg-tab-selected.png | Bin 0 -> 478 bytes src/www/static/img/bg-timestrip-right.png | Bin 0 -> 141 bytes src/www/static/img/bg-timestrip.png | Bin 0 -> 157 bytes src/www/static/img/bg-toolbar.png | Bin 0 -> 283 bytes src/www/static/img/bg.png | Bin 0 -> 684 bytes src/www/static/img/big-info.png | Bin 0 -> 10802 bytes src/www/static/img/blank.gif | Bin 0 -> 43 bytes src/www/static/img/bongo-favicon.gif | Bin 0 -> 547 bytes src/www/static/img/bongo-favicon.ico | Bin 0 -> 473 bytes src/www/static/img/bongo-favicon.png | Bin 0 -> 473 bytes src/www/static/img/cal-offmonth.png | Bin 0 -> 200 bytes src/www/static/img/cal-selected-stipple.png | Bin 0 -> 200 bytes src/www/static/img/cal-selected.png | Bin 0 -> 216 bytes src/www/static/img/cal-today-stipple.png | Bin 0 -> 200 bytes src/www/static/img/cal-today.png | Bin 0 -> 215 bytes src/www/static/img/calendar-16.gif | Bin 0 -> 607 bytes src/www/static/img/calendar-16.png | Bin 0 -> 537 bytes src/www/static/img/cbg.png | Bin 0 -> 411 bytes src/www/static/img/close.gif | Bin 0 -> 109 bytes src/www/static/img/closed-read.png | Bin 0 -> 225 bytes src/www/static/img/closed-unread.png | Bin 0 -> 225 bytes src/www/static/img/contact-16.png | Bin 0 -> 550 bytes src/www/static/img/contact-new.png | Bin 0 -> 2457 bytes src/www/static/img/contact-unknown.png | Bin 0 -> 2243 bytes src/www/static/img/continued-left.png | Bin 0 -> 179 bytes src/www/static/img/continued-right.png | Bin 0 -> 178 bytes src/www/static/img/desktop-big.png | Bin 0 -> 6407 bytes src/www/static/img/dialog-error-32.png | Bin 0 -> 1550 bytes src/www/static/img/dialog-information-32.png | Bin 0 -> 1921 bytes src/www/static/img/dialog-information.gif | Bin 0 -> 590 bytes src/www/static/img/dialog-information.png | Bin 0 -> 556 bytes src/www/static/img/dialog-warning.png | Bin 0 -> 576 bytes src/www/static/img/document-delete.png | Bin 0 -> 783 bytes src/www/static/img/document-properties.png | Bin 0 -> 716 bytes src/www/static/img/dragonfly-favicon.png | Bin 0 -> 680 bytes src/www/static/img/dragonfly.png | Bin 0 -> 6971 bytes src/www/static/img/emblem-readonly.gif | Bin 0 -> 576 bytes src/www/static/img/emblem-readonly.png | Bin 0 -> 615 bytes src/www/static/img/event-16.png | Bin 0 -> 776 bytes src/www/static/img/form_buttons.gif | Bin 0 -> 5617 bytes src/www/static/img/form_buttons.png | Bin 0 -> 3371 bytes src/www/static/img/go-next.png | Bin 0 -> 220 bytes src/www/static/img/go-previous.png | Bin 0 -> 219 bytes src/www/static/img/grad-cal-title.png | Bin 0 -> 957 bytes src/www/static/img/grad-cal.png | Bin 0 -> 423 bytes src/www/static/img/grad-page.png | Bin 0 -> 2144 bytes src/www/static/img/grad-tab.png | Bin 0 -> 369 bytes src/www/static/img/grad-textarea.png | Bin 0 -> 218 bytes src/www/static/img/grad-toolbar.png | Bin 0 -> 394 bytes src/www/static/img/grad.png | Bin 0 -> 415 bytes src/www/static/img/grippy-h.gif | Bin 0 -> 80 bytes src/www/static/img/grippy-h.png | Bin 0 -> 168 bytes src/www/static/img/grippy-v.gif | Bin 0 -> 91 bytes src/www/static/img/grippy-v.png | Bin 0 -> 168 bytes src/www/static/img/grippy.png | Bin 0 -> 208 bytes src/www/static/img/help-browser.png | Bin 0 -> 2343 bytes src/www/static/img/histogram.png | Bin 0 -> 1032 bytes src/www/static/img/home-16.gif | Bin 0 -> 599 bytes src/www/static/img/home-16.png | Bin 0 -> 525 bytes src/www/static/img/icon-simple-calendars.gif | Bin 0 -> 218 bytes src/www/static/img/icon-simple-calendars.png | Bin 0 -> 304 bytes src/www/static/img/icon-simple-contacts.gif | Bin 0 -> 352 bytes src/www/static/img/icon-simple-contacts.png | Bin 0 -> 431 bytes src/www/static/img/icon-simple-todos.gif | Bin 0 -> 257 bytes src/www/static/img/icon-simple-todos.png | Bin 0 -> 367 bytes src/www/static/img/image-x-generic.png | Bin 0 -> 532 bytes src/www/static/img/info-bl.gif | Bin 0 -> 598 bytes src/www/static/img/info-bl.png | Bin 0 -> 500 bytes src/www/static/img/info-br.gif | Bin 0 -> 259 bytes src/www/static/img/info-br.png | Bin 0 -> 389 bytes src/www/static/img/info-cl.gif | Bin 0 -> 281 bytes src/www/static/img/info-cl.png | Bin 0 -> 304 bytes src/www/static/img/info-cr.gif | Bin 0 -> 124 bytes src/www/static/img/info-cr.png | Bin 0 -> 214 bytes src/www/static/img/info-pointer-down.gif | Bin 0 -> 422 bytes src/www/static/img/info-pointer-down.png | Bin 0 -> 876 bytes src/www/static/img/info-pointer-left.gif | Bin 0 -> 679 bytes src/www/static/img/info-pointer-left.png | Bin 0 -> 957 bytes src/www/static/img/info-pointer-right.gif | Bin 0 -> 676 bytes src/www/static/img/info-pointer-right.png | Bin 0 -> 992 bytes src/www/static/img/info-pointer-up.gif | Bin 0 -> 696 bytes src/www/static/img/info-pointer-up.png | Bin 0 -> 864 bytes src/www/static/img/info-tl.gif | Bin 0 -> 926 bytes src/www/static/img/info-tl.png | Bin 0 -> 481 bytes src/www/static/img/info-tr.gif | Bin 0 -> 400 bytes src/www/static/img/info-tr.png | Bin 0 -> 419 bytes src/www/static/img/lightbulb.png | Bin 0 -> 1233 bytes .../static/img/link-underline-view-active.gif | Bin 0 -> 196 bytes src/www/static/img/link-underline-view.gif | Bin 0 -> 78 bytes src/www/static/img/list-add.png | Bin 0 -> 400 bytes src/www/static/img/list-remove.png | Bin 0 -> 215 bytes src/www/static/img/locale.png | Bin 0 -> 587 bytes src/www/static/img/locked.png | Bin 0 -> 612 bytes src/www/static/img/login-bg-bottom.png | Bin 0 -> 1252 bytes src/www/static/img/login-bg-middle.png | Bin 0 -> 442 bytes src/www/static/img/login-bg-top.png | Bin 0 -> 921 bytes src/www/static/img/login-logo.gif | Bin 0 -> 3434 bytes src/www/static/img/login-logo.png | Bin 0 -> 5110 bytes src/www/static/img/login-pane.png | Bin 0 -> 49978 bytes src/www/static/img/login-pane.xcf | Bin 0 -> 157678 bytes src/www/static/img/mail-closed-16.png | Bin 0 -> 537 bytes src/www/static/img/mail-forward-16.png | Bin 0 -> 545 bytes src/www/static/img/mail-inbox-16.png | Bin 0 -> 476 bytes src/www/static/img/mail-inbox-22.png | Bin 0 -> 679 bytes src/www/static/img/mail-reply-all-16.png | Bin 0 -> 586 bytes src/www/static/img/mail-reply-sender-16.png | Bin 0 -> 466 bytes src/www/static/img/message-16.gif | Bin 0 -> 356 bytes src/www/static/img/message-16.png | Bin 0 -> 771 bytes src/www/static/img/message-closed-16.png | Bin 0 -> 417 bytes src/www/static/img/minus.png | Bin 0 -> 540 bytes src/www/static/img/msg-gradient.png | Bin 0 -> 206 bytes src/www/static/img/navselhilight.png | Bin 0 -> 264 bytes src/www/static/img/network-server.png | Bin 0 -> 1202 bytes src/www/static/img/open-read.png | Bin 0 -> 216 bytes src/www/static/img/open-unread.png | Bin 0 -> 216 bytes src/www/static/img/plus.png | Bin 0 -> 523 bytes src/www/static/img/popup.png | Bin 0 -> 6777 bytes src/www/static/img/read-gradient.png | Bin 0 -> 192 bytes src/www/static/img/sad.png | Bin 0 -> 13166 bytes src/www/static/img/sbg.png | Bin 0 -> 32997 bytes src/www/static/img/sel.png | Bin 0 -> 584 bytes src/www/static/img/side-bl.png | Bin 0 -> 577 bytes src/www/static/img/side-br.png | Bin 0 -> 293 bytes src/www/static/img/side-cl.png | Bin 0 -> 271 bytes src/www/static/img/side-cr.png | Bin 0 -> 187 bytes src/www/static/img/side-tl.png | Bin 0 -> 500 bytes src/www/static/img/side-tr.png | Bin 0 -> 384 bytes src/www/static/img/star-widget.gif | Bin 0 -> 866 bytes src/www/static/img/star-widget.png | Bin 0 -> 1576 bytes src/www/static/img/star.png | Bin 0 -> 707 bytes src/www/static/img/system-installer.png | Bin 0 -> 1589 bytes src/www/static/img/system-search.gif | Bin 0 -> 1035 bytes src/www/static/img/system-search.png | Bin 0 -> 935 bytes src/www/static/img/system-users.png | Bin 0 -> 1901 bytes src/www/static/img/text-x-generic.png | Bin 0 -> 333 bytes src/www/static/img/top-gradient.png | Bin 0 -> 239 bytes src/www/static/img/unread-gradient.png | Bin 0 -> 168 bytes src/www/static/img/user-desktop.png | Bin 0 -> 1372 bytes src/www/static/img/user.png | Bin 0 -> 762 bytes src/www/static/img/x.png | Bin 0 -> 1645 bytes src/www/static/index.html | 106 + src/www/static/login-bg-bottom.png | Bin 0 -> 1252 bytes src/www/static/login-bg-middle.png | Bin 0 -> 442 bytes src/www/static/login-bg-top.png | Bin 0 -> 921 bytes src/www/static/login-logo.png | Bin 0 -> 5110 bytes src/www/static/mail-inbox-22.png | Bin 0 -> 679 bytes src/www/static/top-gradient.png | Bin 0 -> 239 bytes src/www/tests/test_calendar_store.py | 122 + src/www/tests/test_card_store.py | 98 + src/www/tests/test_compose.py | 83 + src/www/tests/test_dav.py | 85 + src/www/tests/test_filters.py | 41 + src/www/tests/test_html_mail.py | 49 + src/www/tests/test_mail_store.py | 87 + src/www/tests/test_tasks.py | 38 + 516 files changed, 16029 insertions(+), 24385 deletions(-) create mode 100644 ROADMAP.md create mode 100644 cmake/EmbedFile.cmake create mode 100644 cmake/FindOpenDKIM.cmake create mode 100644 cmake/FindOpenDMARC.cmake create mode 100644 cmake/FindSPF2.cmake create mode 100644 cmake/FindSRS2.cmake create mode 100644 docs/antispam.md create mode 100644 include/bongosasl.h create mode 100644 include/bongosieve.h create mode 100644 init/bongo.service.in create mode 100644 init/bongo.sysusers.in create mode 100644 init/bongo.tmpfiles.in create mode 100644 po/bongo-web/de.po create mode 100644 src/agents/collector/CMakeLists.txt create mode 100644 src/agents/collector/external-accounts.sql create mode 100644 src/agents/collector/external_accounts.c create mode 100644 src/agents/collector/external_accounts.h create mode 100644 src/agents/collector/provider-presets.json create mode 100644 src/agents/sieve/CMakeLists.txt create mode 100644 src/agents/sieve/managesieved.c create mode 100644 src/apps/config/config/sieve create mode 100644 src/libs/mailauth/CMakeLists.txt create mode 100644 src/libs/mailauth/dkim.c create mode 100644 src/libs/mailauth/dmarc.c create mode 100644 src/libs/mailauth/mailauth.c create mode 100644 src/libs/mailauth/mailauth.h create mode 100644 src/libs/mailauth/spf.c create mode 100644 src/libs/mailauth/srs.c create mode 100644 src/libs/mailauth/tests/dkim-test.c create mode 100644 src/libs/mailauth/tests/spf-test.c create mode 100644 src/libs/mailauth/tests/srs-test.c delete mode 100644 src/libs/python/bongo/external/dateutil/__init__.py delete mode 100644 src/libs/python/bongo/external/dateutil/easter.py delete mode 100644 src/libs/python/bongo/external/dateutil/parser.py delete mode 100644 src/libs/python/bongo/external/dateutil/relativedelta.py delete mode 100644 src/libs/python/bongo/external/dateutil/rrule.py delete mode 100644 src/libs/python/bongo/external/dateutil/tz.py delete mode 100644 src/libs/python/bongo/external/dateutil/tzwin.py delete mode 100644 src/libs/python/bongo/external/dateutil/zoneinfo/__init__.py delete mode 100644 src/libs/python/bongo/external/dateutil/zoneinfo/zoneinfo-2005q.tar.gz delete mode 100644 src/libs/python/bongo/external/email/__init__.py delete mode 100644 src/libs/python/bongo/external/email/_parseaddr.py delete mode 100644 src/libs/python/bongo/external/email/base64mime.py delete mode 100644 src/libs/python/bongo/external/email/charset.py delete mode 100644 src/libs/python/bongo/external/email/encoders.py delete mode 100644 src/libs/python/bongo/external/email/errors.py delete mode 100644 src/libs/python/bongo/external/email/feedparser.py delete mode 100644 src/libs/python/bongo/external/email/filegenerator.py delete mode 100644 src/libs/python/bongo/external/email/generator.py delete mode 100644 src/libs/python/bongo/external/email/header.py delete mode 100644 src/libs/python/bongo/external/email/iterators.py delete mode 100644 src/libs/python/bongo/external/email/message.py delete mode 100644 src/libs/python/bongo/external/email/mime/__init__.py delete mode 100644 src/libs/python/bongo/external/email/mime/application.py delete mode 100644 src/libs/python/bongo/external/email/mime/audio.py delete mode 100644 src/libs/python/bongo/external/email/mime/base.py delete mode 100644 src/libs/python/bongo/external/email/mime/image.py delete mode 100644 src/libs/python/bongo/external/email/mime/message.py delete mode 100644 src/libs/python/bongo/external/email/mime/multipart.py delete mode 100644 src/libs/python/bongo/external/email/mime/nonmultipart.py delete mode 100644 src/libs/python/bongo/external/email/mime/text.py delete mode 100644 src/libs/python/bongo/external/email/parser.py delete mode 100644 src/libs/python/bongo/external/email/payloads.py delete mode 100644 src/libs/python/bongo/external/email/quoprimime.py delete mode 100644 src/libs/python/bongo/external/email/test/__init__.py delete mode 100644 src/libs/python/bongo/external/email/test/data/PyBanner048.gif delete mode 100644 src/libs/python/bongo/external/email/test/data/audiotest.au delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_01.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_02.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_03.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_04.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_05.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_06.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_07.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_08.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_09.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_10.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_11.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_12.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_12a.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_13.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_14.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_15.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_16.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_17.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_19.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_20.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_21.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_22.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_23.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_24.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_25.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_26.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_27.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_28.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_29.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_30.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_31.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_32.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_33.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_34.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_35.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_36.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_37.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_38.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_39.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_40.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_41.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_42.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_43.txt delete mode 100644 src/libs/python/bongo/external/email/test/data/msg_44.txt delete mode 100644 src/libs/python/bongo/external/email/test/test_email.py delete mode 100644 src/libs/python/bongo/external/email/test/test_email_codecs.py delete mode 100644 src/libs/python/bongo/external/email/test/test_email_codecs_renamed.py delete mode 100644 src/libs/python/bongo/external/email/test/test_email_renamed.py delete mode 100644 src/libs/python/bongo/external/email/test/test_email_torture.py delete mode 100644 src/libs/python/bongo/external/email/utils.py delete mode 100644 src/libs/python/bongo/external/simplejson/__init__.py delete mode 100644 src/libs/python/bongo/external/simplejson/decoder.py delete mode 100644 src/libs/python/bongo/external/simplejson/encoder.py delete mode 100644 src/libs/python/bongo/external/simplejson/jsonfilter.py delete mode 100644 src/libs/python/bongo/external/simplejson/scanner.py delete mode 100644 src/libs/python/bongo/external/vobject/__init__.py delete mode 100644 src/libs/python/bongo/external/vobject/base.py delete mode 100644 src/libs/python/bongo/external/vobject/behavior.py delete mode 100644 src/libs/python/bongo/external/vobject/icalendar.py delete mode 100644 src/libs/python/bongo/external/vobject/tests.py delete mode 100644 src/libs/python/bongo/external/vobject/vcard.py delete mode 100644 src/libs/python/bongo/external/vobject/win32tz.py create mode 100644 src/libs/python/libbongo/calendar-module.c create mode 100644 src/libs/sasl/CMakeLists.txt create mode 100644 src/libs/sasl/sasl.c create mode 100644 src/libs/sasl/tests/sasl-test.c create mode 100644 src/libs/sieve/CMakeLists.txt create mode 100644 src/libs/sieve/managesieve.c create mode 100644 src/libs/sieve/sieve.c create mode 100644 src/libs/sieve/store.c create mode 100644 src/libs/sieve/tests/managesieve-test.c create mode 100644 src/libs/sieve/tests/sieve-test.c create mode 100644 src/libs/sieve/tests/store-test.c create mode 100644 src/www/CMakeLists.txt create mode 100644 src/www/bongo-web.json create mode 100644 src/www/bongo-web.py.in create mode 100644 src/www/bongo_web/__init__.py create mode 100644 src/www/bongo_web/auth.py create mode 100644 src/www/bongo_web/calendar_store.py create mode 100644 src/www/bongo_web/card_store.py create mode 100644 src/www/bongo_web/compose.py create mode 100644 src/www/bongo_web/dav.py create mode 100644 src/www/bongo_web/filters.py create mode 100644 src/www/bongo_web/html_mail.py create mode 100644 src/www/bongo_web/mail_store.py create mode 100644 src/www/bongo_web/session.py create mode 100644 src/www/bongo_web/tasks.py create mode 100755 src/www/static/Bongo-logo.png create mode 100644 src/www/static/agent-control.png create mode 100644 src/www/static/app.css create mode 100644 src/www/static/app.js create mode 100644 src/www/static/bg-page.png create mode 100644 src/www/static/bg-selected.png create mode 100644 src/www/static/calendar-16.png create mode 100644 src/www/static/contact-new.png create mode 100644 src/www/static/css/admin.css create mode 100644 src/www/static/css/calendar.css create mode 100644 src/www/static/css/composer.css create mode 100644 src/www/static/css/contacts.css create mode 100644 src/www/static/css/conversation.css create mode 100644 src/www/static/css/layout.css create mode 100644 src/www/static/css/log.css create mode 100644 src/www/static/css/login.css create mode 100644 src/www/static/css/print.css create mode 100644 src/www/static/css/summary.css create mode 100644 src/www/static/css/widgets.css create mode 100644 src/www/static/dragonfly.png create mode 100644 src/www/static/favicon.ico create mode 100755 src/www/static/img/Bongo-logo.png create mode 100644 src/www/static/img/accessories-text-editor.gif create mode 100644 src/www/static/img/accessories-text-editor.png create mode 100644 src/www/static/img/addressbook-16.png create mode 100644 src/www/static/img/agent-bongoantispam.png create mode 100644 src/www/static/img/agent-bongoavirus.png create mode 100644 src/www/static/img/agent-bongocalcmd.png create mode 100644 src/www/static/img/agent-bongocollector.png create mode 100644 src/www/static/img/agent-bongoimap.png create mode 100644 src/www/static/img/agent-bongomailprox.png create mode 100644 src/www/static/img/agent-bongopluspack.png create mode 100644 src/www/static/img/agent-bongopop3.png create mode 100644 src/www/static/img/agent-bongoqueue.png create mode 100644 src/www/static/img/agent-bongosmtp.png create mode 100644 src/www/static/img/agent-control.png create mode 100644 src/www/static/img/agent-global.png create mode 100644 src/www/static/img/agent-unknown.png create mode 100644 src/www/static/img/airplane-emblem.png create mode 100644 src/www/static/img/ajax-loader.gif create mode 100644 src/www/static/img/applications-internet.gif create mode 100644 src/www/static/img/applications-internet.png create mode 100644 src/www/static/img/arrow.png create mode 100644 src/www/static/img/bg-button-pressed.png create mode 100644 src/www/static/img/bg-button-selected.png create mode 100644 src/www/static/img/bg-button.png create mode 100644 src/www/static/img/bg-header.png create mode 100644 src/www/static/img/bg-page.png create mode 100644 src/www/static/img/bg-selected.png create mode 100644 src/www/static/img/bg-shine.png create mode 100644 src/www/static/img/bg-side-title-hover.png create mode 100644 src/www/static/img/bg-side-title.png create mode 100644 src/www/static/img/bg-tab-selected.png create mode 100644 src/www/static/img/bg-timestrip-right.png create mode 100644 src/www/static/img/bg-timestrip.png create mode 100644 src/www/static/img/bg-toolbar.png create mode 100755 src/www/static/img/bg.png create mode 100644 src/www/static/img/big-info.png create mode 100644 src/www/static/img/blank.gif create mode 100644 src/www/static/img/bongo-favicon.gif create mode 100644 src/www/static/img/bongo-favicon.ico create mode 100644 src/www/static/img/bongo-favicon.png create mode 100644 src/www/static/img/cal-offmonth.png create mode 100644 src/www/static/img/cal-selected-stipple.png create mode 100644 src/www/static/img/cal-selected.png create mode 100644 src/www/static/img/cal-today-stipple.png create mode 100644 src/www/static/img/cal-today.png create mode 100644 src/www/static/img/calendar-16.gif create mode 100644 src/www/static/img/calendar-16.png create mode 100755 src/www/static/img/cbg.png create mode 100644 src/www/static/img/close.gif create mode 100644 src/www/static/img/closed-read.png create mode 100644 src/www/static/img/closed-unread.png create mode 100644 src/www/static/img/contact-16.png create mode 100644 src/www/static/img/contact-new.png create mode 100644 src/www/static/img/contact-unknown.png create mode 100644 src/www/static/img/continued-left.png create mode 100644 src/www/static/img/continued-right.png create mode 100644 src/www/static/img/desktop-big.png create mode 100644 src/www/static/img/dialog-error-32.png create mode 100644 src/www/static/img/dialog-information-32.png create mode 100644 src/www/static/img/dialog-information.gif create mode 100644 src/www/static/img/dialog-information.png create mode 100644 src/www/static/img/dialog-warning.png create mode 100644 src/www/static/img/document-delete.png create mode 100644 src/www/static/img/document-properties.png create mode 100644 src/www/static/img/dragonfly-favicon.png create mode 100644 src/www/static/img/dragonfly.png create mode 100644 src/www/static/img/emblem-readonly.gif create mode 100644 src/www/static/img/emblem-readonly.png create mode 100644 src/www/static/img/event-16.png create mode 100644 src/www/static/img/form_buttons.gif create mode 100644 src/www/static/img/form_buttons.png create mode 100644 src/www/static/img/go-next.png create mode 100644 src/www/static/img/go-previous.png create mode 100644 src/www/static/img/grad-cal-title.png create mode 100644 src/www/static/img/grad-cal.png create mode 100644 src/www/static/img/grad-page.png create mode 100644 src/www/static/img/grad-tab.png create mode 100644 src/www/static/img/grad-textarea.png create mode 100644 src/www/static/img/grad-toolbar.png create mode 100644 src/www/static/img/grad.png create mode 100644 src/www/static/img/grippy-h.gif create mode 100644 src/www/static/img/grippy-h.png create mode 100644 src/www/static/img/grippy-v.gif create mode 100644 src/www/static/img/grippy-v.png create mode 100644 src/www/static/img/grippy.png create mode 100644 src/www/static/img/help-browser.png create mode 100644 src/www/static/img/histogram.png create mode 100644 src/www/static/img/home-16.gif create mode 100644 src/www/static/img/home-16.png create mode 100644 src/www/static/img/icon-simple-calendars.gif create mode 100644 src/www/static/img/icon-simple-calendars.png create mode 100644 src/www/static/img/icon-simple-contacts.gif create mode 100644 src/www/static/img/icon-simple-contacts.png create mode 100644 src/www/static/img/icon-simple-todos.gif create mode 100644 src/www/static/img/icon-simple-todos.png create mode 100644 src/www/static/img/image-x-generic.png create mode 100644 src/www/static/img/info-bl.gif create mode 100644 src/www/static/img/info-bl.png create mode 100644 src/www/static/img/info-br.gif create mode 100644 src/www/static/img/info-br.png create mode 100644 src/www/static/img/info-cl.gif create mode 100644 src/www/static/img/info-cl.png create mode 100644 src/www/static/img/info-cr.gif create mode 100644 src/www/static/img/info-cr.png create mode 100644 src/www/static/img/info-pointer-down.gif create mode 100644 src/www/static/img/info-pointer-down.png create mode 100644 src/www/static/img/info-pointer-left.gif create mode 100644 src/www/static/img/info-pointer-left.png create mode 100644 src/www/static/img/info-pointer-right.gif create mode 100644 src/www/static/img/info-pointer-right.png create mode 100644 src/www/static/img/info-pointer-up.gif create mode 100644 src/www/static/img/info-pointer-up.png create mode 100644 src/www/static/img/info-tl.gif create mode 100644 src/www/static/img/info-tl.png create mode 100644 src/www/static/img/info-tr.gif create mode 100644 src/www/static/img/info-tr.png create mode 100644 src/www/static/img/lightbulb.png create mode 100644 src/www/static/img/link-underline-view-active.gif create mode 100644 src/www/static/img/link-underline-view.gif create mode 100644 src/www/static/img/list-add.png create mode 100644 src/www/static/img/list-remove.png create mode 100644 src/www/static/img/locale.png create mode 100644 src/www/static/img/locked.png create mode 100644 src/www/static/img/login-bg-bottom.png create mode 100644 src/www/static/img/login-bg-middle.png create mode 100644 src/www/static/img/login-bg-top.png create mode 100644 src/www/static/img/login-logo.gif create mode 100644 src/www/static/img/login-logo.png create mode 100644 src/www/static/img/login-pane.png create mode 100644 src/www/static/img/login-pane.xcf create mode 100644 src/www/static/img/mail-closed-16.png create mode 100644 src/www/static/img/mail-forward-16.png create mode 100644 src/www/static/img/mail-inbox-16.png create mode 100644 src/www/static/img/mail-inbox-22.png create mode 100644 src/www/static/img/mail-reply-all-16.png create mode 100644 src/www/static/img/mail-reply-sender-16.png create mode 100644 src/www/static/img/message-16.gif create mode 100644 src/www/static/img/message-16.png create mode 100644 src/www/static/img/message-closed-16.png create mode 100644 src/www/static/img/minus.png create mode 100644 src/www/static/img/msg-gradient.png create mode 100644 src/www/static/img/navselhilight.png create mode 100644 src/www/static/img/network-server.png create mode 100644 src/www/static/img/open-read.png create mode 100644 src/www/static/img/open-unread.png create mode 100644 src/www/static/img/plus.png create mode 100644 src/www/static/img/popup.png create mode 100644 src/www/static/img/read-gradient.png create mode 100644 src/www/static/img/sad.png create mode 100644 src/www/static/img/sbg.png create mode 100755 src/www/static/img/sel.png create mode 100644 src/www/static/img/side-bl.png create mode 100644 src/www/static/img/side-br.png create mode 100644 src/www/static/img/side-cl.png create mode 100644 src/www/static/img/side-cr.png create mode 100644 src/www/static/img/side-tl.png create mode 100644 src/www/static/img/side-tr.png create mode 100644 src/www/static/img/star-widget.gif create mode 100644 src/www/static/img/star-widget.png create mode 100644 src/www/static/img/star.png create mode 100644 src/www/static/img/system-installer.png create mode 100644 src/www/static/img/system-search.gif create mode 100644 src/www/static/img/system-search.png create mode 100644 src/www/static/img/system-users.png create mode 100644 src/www/static/img/text-x-generic.png create mode 100644 src/www/static/img/top-gradient.png create mode 100644 src/www/static/img/unread-gradient.png create mode 100644 src/www/static/img/user-desktop.png create mode 100644 src/www/static/img/user.png create mode 100644 src/www/static/img/x.png create mode 100644 src/www/static/index.html create mode 100644 src/www/static/login-bg-bottom.png create mode 100644 src/www/static/login-bg-middle.png create mode 100644 src/www/static/login-bg-top.png create mode 100644 src/www/static/login-logo.png create mode 100644 src/www/static/mail-inbox-22.png create mode 100644 src/www/static/top-gradient.png create mode 100644 src/www/tests/test_calendar_store.py create mode 100644 src/www/tests/test_card_store.py create mode 100644 src/www/tests/test_compose.py create mode 100644 src/www/tests/test_dav.py create mode 100644 src/www/tests/test_filters.py create mode 100644 src/www/tests/test_html_mail.py create mode 100644 src/www/tests/test_mail_store.py create mode 100644 src/www/tests/test_tasks.py diff --git a/CMakeLists.txt b/CMakeLists.txt index c2054bc..f71efdb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,18 +1,22 @@ # Global build file for Bongo -project(bongo C) +cmake_minimum_required(VERSION 3.10) +project(bongo VERSION 0.7.0 LANGUAGES C) +if(POLICY CMP0177) + cmake_policy(SET CMP0177 NEW) +endif() +include(CTest) # set version of cmake required, and module path -cmake_minimum_required(VERSION 2.6) -cmake_policy(VERSION 2.6) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${CMAKE_SOURCE_DIR}/cmake/") -set(BONGO_V_MAJOR 0) -set(BONGO_V_MINOR 1) -set(BONGO_V_BUILD 0) -set(BONGO_V_STR "${BONGO_V_MAJOR}.${BONGO_V_MINOR}.${BONGO_V_BUILD}") +set(BONGO_V_MAJOR "${PROJECT_VERSION_MAJOR}") +set(BONGO_V_MINOR "${PROJECT_VERSION_MINOR}") +set(BONGO_V_BUILD "${PROJECT_VERSION_PATCH}") +set(BONGO_V_PATCH "${PROJECT_VERSION_PATCH}") +set(BONGO_V_STR "${PROJECT_VERSION}") # arguments we can supply to the build system - TODO -set(BONGO_USER "root" CACHE STRING "User account Bongo should run under") +set(BONGO_USER "bongo" CACHE STRING "User account Bongo should run under") option(CONN_TRACE "Enable connection tracing" OFF) include(cmake/BongoCompiler.cmake) @@ -42,9 +46,13 @@ include_directories(BEFORE ${CMAKE_CURRENT_BINARY_DIR}/include/) # now build our various libraries, agents, binaries. # DO NOT ALTER (SO)VERSION ON LIBRARIES WITHOUT ASKING! -foreach (LIBRARY xpl streamio msgapi connio util json cal) +foreach (LIBRARY xpl streamio msgapi connio util json cal mailauth sieve sasl) add_subdirectory (src/libs/${LIBRARY}) - set_target_properties(bongo${LIBRARY} PROPERTIES + set(LIBRARY_TARGET bongo${LIBRARY}) + if(LIBRARY STREQUAL "sieve") + set(LIBRARY_TARGET libbongosieve) + endif() + set_target_properties(${LIBRARY_TARGET} PROPERTIES SOVERSION 0 VERSION ${BONGO_V_STR} ) @@ -53,7 +61,7 @@ endforeach (LIBRARY) add_subdirectory(src/libs/python/libbongo) add_subdirectory(src/libs/python/bongo) -foreach (AGENT antispam avirus director imap pop queue rules smtp store) +foreach (AGENT antispam avirus collector director imap pop queue rules sieve smtp store) add_subdirectory (src/agents/${AGENT}) endforeach (AGENT) @@ -61,6 +69,8 @@ foreach (EXECUTABLE config manager testtool queuetool storetool sendmail) add_subdirectory (src/apps/${EXECUTABLE}) endforeach (EXECUTABLE) +add_subdirectory(src/www) + add_subdirectory (init) # install various other files @@ -74,5 +84,12 @@ install(DIRECTORY zoneinfo FILES_MATCHING PATTERN "*.zone" PATTERN ".svn" EXCLUDE) +install(DIRECTORY DESTINATION ${XPL_DEFAULT_DBF_DIR} + DIRECTORY_PERMISSIONS OWNER_READ OWNER_WRITE OWNER_EXECUTE + GROUP_READ GROUP_EXECUTE) + +install(FILES src/agents/collector/provider-presets.json + DESTINATION ${XPL_DEFAULT_DATA_DIR}/providers) + # include rules for make distribution tarballs include(cmake/Distribution.cmake) diff --git a/README b/README index f17a57f..5714725 100644 --- a/README +++ b/README @@ -1,7 +1,21 @@ -##### Ths project is no longer maintained #### -##### It is left here for refernce purposes #### - -Bongo is an easy-to-use mail and calendar system, offering a simple yet powerful user interface. +Bongo is an easy-to-use mail and calendar system, offering a simple yet powerful user interface. The goal is to make sharing, organisation, and communication simpler, quicker, and more useful. -You can find out more on http://www.bongo-project.org +This repository contains the maintained Python 3 and CMake based Bongo 0.7 +development line. + +Build requirements include a C11 compiler, CMake, Python 3 with development +headers, python-dateutil, vobject, nh3, GnuTLS, SQLite, Cyrus SASL, GNU +Mailutils, libical and the libraries detected by CMake. Optional mail +authentication support is enabled when libspf2, OpenDKIM, OpenDMARC and libsrs2 +are installed. + +Typical build and test commands are: + + cmake -S . -B build -DCMAKE_BUILD_TYPE=Release + cmake --build build + ctest --test-dir build --output-on-failure + +Use `-DCMAKE_INSTALL_PREFIX=/usr` for distribution packages. CPack source +archives are named `bongo-.tar.*`; platform names are reserved for +binary archives. diff --git a/ROADMAP.md b/ROADMAP.md new file mode 100644 index 0000000..3e0b872 --- /dev/null +++ b/ROADMAP.md @@ -0,0 +1,127 @@ +# Bongo roadmap + +## 0.7.0 implementation status (2026-07-16) + +Completed foundations: CMake/CPack packaging, GCC/Clang warning cleanup, +Python 3.14 compatibility, system vobject/dateutil migration, SMTP/IMAP/POP +build targets, SPF/DKIM/DMARC/SRS libraries, Cyrus SASL PLAIN/LOGIN support, +RFC 5228 validation/ManageSieve scaffolding, systemd templates, and sanitizer +test coverage. + +Still required before the 0.7.0 release: production integration of external +account collection, internal relay policy and normalization, user filter/task +and signature storage, complete IMAP/POP/SMTP capability work, TOTP and app +passwords, complete CalDAV/CardDAV behavior, interoperability tests, and +distribution Ebuilds/packages. No release tag is valid until these items are +implemented and tested. + +## Required for Bongo 0.7.0: modern mail protocols + +- Separate SMTP relay on port 25 from authenticated submission on ports 587 + (STARTTLS) and 465 (implicit TLS). Submission must require authentication by + default and use its own relay and sender policies. +- Add an optional internal relay listener (port 26 by default) for appliances + and trusted services. It must be disabled by default, bind independently and + refuse to start without an explicit source-network allowlist. +- Configure the internal relay through the installation wizard, the web admin + interface and the console configuration tool. All three must share one schema + and validation for bind address, networks, normalization domain, explicit + device mappings, verified DNS suffixes, TLS compatibility and rate limits. +- Add external mail accounts as per-user database objects, never global server + configuration. One account combines inbound IMAP/POP collection settings, + retention/deletion policy, folder mapping and duplicate tracking with its + authenticated outbound SMTP identity. Store provider passwords, app passwords + and SMTP tokens only in the encrypted credential store. Users manage their own + accounts in web settings; administrators control policy and may assist through + admin and console interfaces. Include provider connection tests, certificate + verification, secret rotation and explicit Proton Bridge/SMTP-token modes. +- Detect mailing lists primarily from RFC 2919 `List-ID` and RFC 2369 + `List-Post` headers. Expose filter conditions for any mailing list and for a + specific canonical List-ID; existing move/copy actions then file messages in + a user-selected mail folder. +- Show a per-user `!` task when a new List-ID is first detected. From that task + the user can create a folder rule, ignore it once, permanently disable future + prompts for that List-ID, or delete the task. Deleting a task must never + delete its message; message deletion is a separate explicit action. Persist + task state by user and canonical List-ID so ignored/disabled prompts do not + reappear, and provide equivalent controls in web settings. +- Provide a general per-user filter editor. Conditions include sender address, + sender domain, recipient, subject, arbitrary headers, message size, mailing + list/List-ID and membership of the sender in a contact group (for example, + group `Family`). Actions include move/copy to a folder, forward, delete and + stop processing. Validate every rule server-side and never grant the web + process unrestricted write access to the system configuration store. +- Add per-user plain-text and HTML signatures to the restored web composer. + Allow multiple signatures and select a default per sender identity or + external account. Never append a personal signature a second time to mail + composed by IMAP/SMTP clients. Keep PlusPack signatures as a separate, + administrator-controlled organization disclaimer feature. +- Use RFC 5228 Sieve as the public filter import/export format and provide an + RFC 5804 ManageSieve service on port 4190 with STARTTLS and secure SASL. + Web filters, imported scripts and ManageSieve must share one parser, + capability set, validator and active-script store; unsupported extensions + must produce an explicit error rather than being silently discarded. +- Provide a native RFC 5230 `vacation` action and RFC 6131 + `vacation-seconds` for the web out-of-office assistant. Bongo must own + sender suppression, loop prevention, rate limiting, reply generation and + audit logging; do not execute Mailutils' external vacation delivery module. +- Use Cyrus SASL as the common authentication mechanism implementation for + SMTP submission, IMAP, POP3 and ManageSieve. Keep authorization, app-password + policy and auditing in Bongo callbacks; do not duplicate PLAIN, SCRAM or + OAUTHBEARER protocol implementations in individual agents. +- Support SASL PLAIN and LOGIN over TLS, SASL initial responses, SCRAM-SHA-256, + app passwords and optional OAUTHBEARER/EXTERNAL. Never advertise mechanisms + that are unavailable or unsafe on the current connection. +- Add POP3 SASL and integrate its authentication with app-password policy. +- Add IMAP IDLE, UIDPLUS, MOVE, SPECIAL-USE, LIST-EXTENDED and LITERAL+. +- Add IMAP ENABLE and UTF8=ACCEPT, then advertise IMAP4rev2 only after every + mandatory rev2 behavior and interoperability test passes. +- Implement SMTPUTF8 end to end through submission, relay, LMTP, queue, DSN, + DKIM and SRS handling. +- Add outbound certificate verification and policy support for DANE, MTA-STS, + TLS-RPT and REQUIRETLS. +- Add CONDSTORE/QRESYNC, ESEARCH/SEARCHRES, SORT/THREAD, BINARY and APPENDLIMIT. +- Cover every advertised capability with protocol tests and Thunderbird + interoperability tests. + +## Required for Bongo 0.7.0: authentication hardening + +- Add optional TOTP two-factor authentication for the web interface and + administration. +- Add single-display recovery codes and a secure recovery/reset workflow. +- Add individually revocable app passwords for IMAP, POP3, SMTP submission, + CalDAV and CardDAV. +- Allow each app password to be restricted by service, expiry time and, + optionally, source network. +- Store app passwords with Argon2id and encrypt TOTP secrets at rest. Never + store or display reusable plaintext credentials. +- When 2FA is enabled, allow administrators to require app passwords and + disable the account password for non-interactive protocols. +- Record creation, last use, service and source address without logging the + credential itself. + +Possible implementation dependencies are liboath, libargon2 and optional +libqrencode. This work is a release requirement for Bongo 0.7.0 and must be +implemented and tested after CalDAV/CardDAV and the web restoration, before +packaging and release. + +## Groupware + +- Complete CalDAV event discovery, synchronization and CRUD operations. +- Add CardDAV contacts using Bongo's existing address-book store. +- Add Thunderbird-oriented setup and RFC 6764 service discovery. +- Keep Outlook/MAPI/OpenChange integration deferred until after 0.7.0. Preserve + stable service boundaries and identifiers in authentication, contacts, + calendars, folders and filters so that a future MAPI compatibility layer can + be added without coupling it to the web UI or replacing the core data model. + +## External library cleanup + +- Prefer maintained system libraries over historical bundled snapshots. +- Keep libical as the native iCalendar parser/converter and update the minimum + supported system version after cross-architecture testing. +- Bongo 0.7 uses system vobject/python-dateutil, Python's `json` and `email` + standard libraries, and no longer ships the historical bundled copies. +- SimpleTAL remains temporarily embedded only for the legacy administration + templates; replace that layer after the restored web administration UI has + equivalent coverage. diff --git a/cmake/Directories.cmake b/cmake/Directories.cmake index 53291dc..a405205 100644 --- a/cmake/Directories.cmake +++ b/cmake/Directories.cmake @@ -21,7 +21,9 @@ set(INCLUDEDIR ${PROJECT_BINARY_DIR}/include) # various paths for where objects get installed / work from set(XPL_DEFAULT_DATA_DIR ${CMAKE_INSTALL_PREFIX}/share/bongo) -set(XPL_DEFAULT_STATE_DIR ${CMAKE_INSTALL_PREFIX}/var/bongo) +set(BONGO_STATE_DIR /var/lib/bongo CACHE PATH + "Persistent Bongo state directory") +set(XPL_DEFAULT_STATE_DIR ${BONGO_STATE_DIR}) set(XPL_DEFAULT_BIN_DIR ${CMAKE_INSTALL_PREFIX}/sbin) set(XPL_DEFAULT_LIB_DIR ${CMAKE_INSTALL_PREFIX}/${LIB_DIR_NAME}) diff --git a/cmake/Distribution.cmake b/cmake/Distribution.cmake index 6a003c1..72f1e2f 100644 --- a/cmake/Distribution.cmake +++ b/cmake/Distribution.cmake @@ -2,10 +2,28 @@ set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Mail and calendering made simple") set(CPACK_PACKAGE_VENDOR "Bongo Project") set(CPACK_PACKAGE_DESCRIPTION_FILE "${CMAKE_CURRENT_SOURCE_DIR}/README") set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/COPYING") -set(CPACK_PACKAGE_VERSION_MAJOR "${BONGO_V_MAJOR}") -set(CPACK_PACKAGE_VERSION_MINOR "${BONGO_V_MINOR}") -set(CPACK_PACKAGE_VERSION_PATCH "${BONGO_V_PATCH}") +set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}") +set(CPACK_PACKAGE_VERSION_MAJOR "${PROJECT_VERSION_MAJOR}") +set(CPACK_PACKAGE_VERSION_MINOR "${PROJECT_VERSION_MINOR}") +set(CPACK_PACKAGE_VERSION_PATCH "${PROJECT_VERSION_PATCH}") +set(CPACK_SOURCE_PACKAGE_FILE_NAME "${PROJECT_NAME}-${PROJECT_VERSION}") +set(CPACK_SOURCE_IGNORE_FILES + "/\\.git/" + "/\\.git$" + "/__pycache__/" + "\\.py[co]$" + "/CMakeFiles/" + "/CMakeCache\\.txt$" + "/cmake_install\\.cmake$" + "/Makefile$" + "~$" + "\\.sw[op]$") set(CPACK_PACKAGE_INSTALL_DIRECTORY "CMake ${CMake_VERSION_MAJOR}.${CMake_VERSION_MINOR}") +# Several legacy install rules use absolute destinations derived from +# CMAKE_INSTALL_PREFIX. Always stage those paths below CPack's temporary +# directory; package generation must never write into the host filesystem. +set(CPACK_SET_DESTDIR ON) + include(CPack) diff --git a/cmake/EmbedFile.cmake b/cmake/EmbedFile.cmake new file mode 100644 index 0000000..c230b9e --- /dev/null +++ b/cmake/EmbedFile.cmake @@ -0,0 +1,18 @@ +if(NOT DEFINED INPUT OR NOT DEFINED OUTPUT OR NOT DEFINED SYMBOL) + message(FATAL_ERROR "EmbedFile.cmake requires INPUT, OUTPUT and SYMBOL") +endif() + +file(READ "${INPUT}" content HEX) +string(REGEX MATCHALL ".." bytes "${content}") +set(source "unsigned char ${SYMBOL}[] = {\n") +set(column 0) +foreach(byte IN LISTS bytes) + string(APPEND source "0x${byte},") + math(EXPR column "${column} + 1") + if(column EQUAL 16) + string(APPEND source "\n") + set(column 0) + endif() +endforeach() +string(APPEND source "0x00\n};\n") +file(WRITE "${OUTPUT}" "${source}") diff --git a/cmake/FindOpenDKIM.cmake b/cmake/FindOpenDKIM.cmake new file mode 100644 index 0000000..5187de0 --- /dev/null +++ b/cmake/FindOpenDKIM.cmake @@ -0,0 +1,14 @@ +find_path(OpenDKIM_INCLUDE_DIR NAMES opendkim/dkim.h) +find_library(OpenDKIM_LIBRARY NAMES opendkim) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(OpenDKIM REQUIRED_VARS OpenDKIM_LIBRARY OpenDKIM_INCLUDE_DIR) + +if(OpenDKIM_FOUND AND NOT TARGET OpenDKIM::OpenDKIM) + add_library(OpenDKIM::OpenDKIM UNKNOWN IMPORTED) + set_target_properties(OpenDKIM::OpenDKIM PROPERTIES + IMPORTED_LOCATION "${OpenDKIM_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${OpenDKIM_INCLUDE_DIR}") +endif() + +mark_as_advanced(OpenDKIM_INCLUDE_DIR OpenDKIM_LIBRARY) diff --git a/cmake/FindOpenDMARC.cmake b/cmake/FindOpenDMARC.cmake new file mode 100644 index 0000000..8ade864 --- /dev/null +++ b/cmake/FindOpenDMARC.cmake @@ -0,0 +1,14 @@ +find_path(OpenDMARC_INCLUDE_DIR NAMES opendmarc/dmarc.h) +find_library(OpenDMARC_LIBRARY NAMES opendmarc) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(OpenDMARC REQUIRED_VARS OpenDMARC_LIBRARY OpenDMARC_INCLUDE_DIR) + +if(OpenDMARC_FOUND AND NOT TARGET OpenDMARC::OpenDMARC) + add_library(OpenDMARC::OpenDMARC UNKNOWN IMPORTED) + set_target_properties(OpenDMARC::OpenDMARC PROPERTIES + IMPORTED_LOCATION "${OpenDMARC_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${OpenDMARC_INCLUDE_DIR}") +endif() + +mark_as_advanced(OpenDMARC_INCLUDE_DIR OpenDMARC_LIBRARY) diff --git a/cmake/FindSPF2.cmake b/cmake/FindSPF2.cmake new file mode 100644 index 0000000..b1d685b --- /dev/null +++ b/cmake/FindSPF2.cmake @@ -0,0 +1,14 @@ +find_path(SPF2_INCLUDE_DIR NAMES spf2/spf.h) +find_library(SPF2_LIBRARY NAMES spf2) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(SPF2 REQUIRED_VARS SPF2_LIBRARY SPF2_INCLUDE_DIR) + +if(SPF2_FOUND AND NOT TARGET SPF2::SPF2) + add_library(SPF2::SPF2 UNKNOWN IMPORTED) + set_target_properties(SPF2::SPF2 PROPERTIES + IMPORTED_LOCATION "${SPF2_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${SPF2_INCLUDE_DIR}") +endif() + +mark_as_advanced(SPF2_INCLUDE_DIR SPF2_LIBRARY) diff --git a/cmake/FindSRS2.cmake b/cmake/FindSRS2.cmake new file mode 100644 index 0000000..5504843 --- /dev/null +++ b/cmake/FindSRS2.cmake @@ -0,0 +1,14 @@ +find_path(SRS2_INCLUDE_DIR NAMES srs2.h) +find_library(SRS2_LIBRARY NAMES srs2) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(SRS2 REQUIRED_VARS SRS2_LIBRARY SRS2_INCLUDE_DIR) + +if(SRS2_FOUND AND NOT TARGET SRS2::SRS2) + add_library(SRS2::SRS2 UNKNOWN IMPORTED) + set_target_properties(SRS2::SRS2 PROPERTIES + IMPORTED_LOCATION "${SRS2_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${SRS2_INCLUDE_DIR}") +endif() + +mark_as_advanced(SRS2_INCLUDE_DIR SRS2_LIBRARY) diff --git a/cmake/Requirements.cmake b/cmake/Requirements.cmake index 7be3fa2..5d8e00a 100644 --- a/cmake/Requirements.cmake +++ b/cmake/Requirements.cmake @@ -82,6 +82,23 @@ pkg_check_modules (SQLITE REQUIRED sqlite3) # check for curl pkg_check_modules (CURL REQUIRED libcurl) +# RFC 5228 Sieve parser and validator used by the filter UI and ManageSieve. +find_path(MAILUTILS_INCLUDE_DIR mailutils/sieve.h REQUIRED) +find_library(MAILUTILS_SIEVE_LIBRARY NAMES mu_sieve REQUIRED) +find_library(MAILUTILS_LIBRARY NAMES mailutils REQUIRED) +set(MAILUTILS_LIBRARIES ${MAILUTILS_SIEVE_LIBRARY} ${MAILUTILS_LIBRARY}) + +# Shared SASL implementation for SMTP, IMAP, POP3 and ManageSieve. +pkg_check_modules(SASL2 REQUIRED libsasl2>=2.1.28) + +# Native mail authentication. Bongo links to the protocol libraries directly; +# it does not require the corresponding milter daemons at runtime. +find_package(SPF2 REQUIRED) +find_package(OpenDKIM REQUIRED) +find_package(OpenDMARC REQUIRED) +find_package(SRS2 REQUIRED) +pkg_check_modules(PSL REQUIRED libpsl) + # check for libical check_include_file(libical/ical.h HAVE_ICAL_H) if (NOT HAVE_ICAL_H) @@ -99,34 +116,43 @@ else (HAVE_ICAL) endif (HAVE_ICAL) # check for Python -include(FindPythonLibs) -if (NOT PYTHONLIBS_FOUND) +find_package(Python3 COMPONENTS Interpreter Development REQUIRED) +set(PYTHON_EXECUTABLE "${Python3_EXECUTABLE}") +set(PYTHON_LIBRARIES "${Python3_LIBRARIES}") +set(PYTHON_INCLUDE_DIRS "${Python3_INCLUDE_DIRS}") +set(PYTHON_INCLUDE_PATH "${Python3_INCLUDE_DIRS}") +function(python_add_module target) + add_library(${target} MODULE ${ARGN}) +endfunction() +if (NOT Python3_Development_FOUND) message(FATAL_ERROR "Bongo requires Python development libraries installed") -endif (NOT PYTHONLIBS_FOUND) +endif () -execute_process(COMMAND python -c "from distutils.sysconfig import get_python_lib; print get_python_lib()" +execute_process( + COMMAND "${Python3_EXECUTABLE}" -c "import dateutil, nh3, vobject" + RESULT_VARIABLE PYTHON_NH3_FOUND +) +if (NOT PYTHON_NH3_FOUND EQUAL 0) + message(FATAL_ERROR "Bongo requires the Python dateutil, nh3, and vobject modules") +endif () + +execute_process(COMMAND "${Python3_EXECUTABLE}" -c "import sysconfig; print(sysconfig.get_path('purelib', vars={'base':'', 'platbase':''}))" RESULT_VARIABLE PYTHON_SITEPACKAGES_FOUND OUTPUT_VARIABLE PYTHON_SITEPACKAGES_PATH_RAW ) string(STRIP "${PYTHON_SITEPACKAGES_PATH_RAW}" PYTHON_SITEPACKAGES_PATH_RAW) -execute_process(COMMAND python -c "from distutils.sysconfig import get_python_lib; print get_python_lib(1)" +execute_process(COMMAND "${Python3_EXECUTABLE}" -c "import sysconfig; print(sysconfig.get_path('platlib', vars={'base':'', 'platbase':''}))" RESULT_VARIABLE PYTHON_SITELIB_FOUND OUTPUT_VARIABLE PYTHON_SITELIB_PATH_RAW ) string(STRIP "${PYTHON_SITELIB_PATH_RAW}" PYTHON_SITELIB_PATH_RAW) -string(LENGTH "${CMAKE_INSTALL_PREFIX}" CMAKE_INSTALL_PREFIX_LEN) -string(SUBSTRING "${PYTHON_SITEPACKAGES_PATH_RAW}" 0 ${CMAKE_INSTALL_PREFIX_LEN} PYTHON_SITEPACKAGES_PATH_RAW_PREFIX) - if (PYTHON_SITEPACKAGES_FOUND EQUAL 0) - if (PYTHON_SITEPACKAGES_PATH_RAW_PREFIX STREQUAL CMAKE_INSTALL_PREFIX) - set(PYTHON_SITEPACKAGES_PATH "${PYTHON_SITEPACKAGES_PATH_RAW}") - set(PYTHON_SITELIB_PATH "${PYTHON_SITELIB_PATH_RAW}") - else () - set(PYTHON_SITEPACKAGES_PATH "${CMAKE_INSTALL_PREFIX}/${PYTHON_SITEPACKAGES_PATH_RAW}") - set(PYTHON_SITELIB_PATH "${CMAKE_INSTALL_PREFIX}/${PYTHON_SITELIB_PATH_RAW}") - endif () + # sysconfig returns paths relative to an empty base with a leading slash. + # Prefix them exactly once so /usr/local never becomes /usr/local/usr. + set(PYTHON_SITEPACKAGES_PATH "${CMAKE_INSTALL_PREFIX}${PYTHON_SITEPACKAGES_PATH_RAW}") + set(PYTHON_SITELIB_PATH "${CMAKE_INSTALL_PREFIX}${PYTHON_SITELIB_PATH_RAW}") else (PYTHON_SITEPACKAGES_FOUND EQUAL 0) message(FATAL_ERROR "Couldn't determine where to install Python modules") endif (PYTHON_SITEPACKAGES_FOUND EQUAL 0) diff --git a/config.h.cmake b/config.h.cmake index 1bd00dd..43f88eb 100644 --- a/config.h.cmake +++ b/config.h.cmake @@ -3,6 +3,7 @@ // our package name #cmakedefine PACKAGE "@CMAKE_PROJECT_NAME@" +#define BONGO_VERSION "@PROJECT_VERSION@" // the way we do includes on the python code seems to cause problems // with the cmake style of defines. @@ -77,7 +78,9 @@ // Legacy defines below #cmakedefine LINUX #cmakedefine UNIX -#cmakedefine HAVE_NANOSLEEP +#ifndef HAVE_NANOSLEEP +#cmakedefine HAVE_NANOSLEEP 1 +#endif #cmakedefine _BONGO_XPL_BIG_ENDIAN #cmakedefine _BONGO_XPL_LITTLE_ENDIAN #cmakedefine _BONGO_HAVE_DLFCN_H diff --git a/docs/antispam.md b/docs/antispam.md new file mode 100644 index 0000000..985d06f --- /dev/null +++ b/docs/antispam.md @@ -0,0 +1,40 @@ +# Spam and malware scanning + +Bongo uses two independent scanners. `bongoantispam` sends mail to a standard +SpamAssassin `spamd` service for content classification. `bongoavirus` uses +ClamAV for malware detection. Neither service replaces the other. + +The antispam agent is fail-open: a connection failure, malformed response or +configured size limit leaves the original message available for normal +delivery and records the problem in the log. It never sends a rejection or +bounce to the apparent sender. A successful SpamAssassin `PROCESS` response +replaces the queued copy with the processed message containing the normal +`X-Spam-*` headers and marks the envelope as already checked. + +The default `antispam` JSON configuration is: + +```json +{ + "version": 1, + "enabled": true, + "timeout": 15, + "maximum_message_size": 52428800, + "maximum_response_size": 53477376, + "hosts": ["127.0.0.1"] +} +``` + +`hosts` accepts the existing `host:port:weight` syntax and can list several +`spamd` servers. Port 783 is used when no port is specified. The response limit +must be at least as large as the input limit because SpamAssassin adds headers. + +Spam folder rules should match SpamAssassin's `X-Spam-Status` header. A high +score quarantine is deliberately not implemented through the historical +global quarantine queue: quarantine state belongs to the recipient and must be +visible and releasable through that user's task list. Until that per-user path +is active, mail is tagged and can be filed by Sieve without being destroyed. + +The systemd unit only orders Bongo after common ClamAV and SpamAssassin unit +names when those services are present. It does not pull either scanner in, so +remote scanners and installations without one of the optional services remain +supported. diff --git a/include/bongoagent.h b/include/bongoagent.h index b49b84e..fffd15a 100644 --- a/include/bongoagent.h +++ b/include/bongoagent.h @@ -195,7 +195,7 @@ struct _BongoGlobals { }; extern struct _BongoGlobals BongoGlobals; -static BongoConfigItem GlobalConfig[] = { +static BongoConfigItem GlobalConfig[] __attribute__((unused)) = { { BONGO_JSON_STRING, "o:hostname/s", &BongoGlobals.hostname }, { BONGO_JSON_STRING, "o:hostaddr/s", &BongoGlobals.hostaddr }, { BONGO_JSON_STRING, "o:postmaster/s", &BongoGlobals.postmaster }, diff --git a/include/bongosasl.h b/include/bongosasl.h new file mode 100644 index 0000000..4898f2d --- /dev/null +++ b/include/bongosasl.h @@ -0,0 +1,40 @@ +#ifndef BONGO_SASL_H +#define BONGO_SASL_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct BongoSaslSession BongoSaslSession; +typedef int (*BongoSaslVerify)(void *context, const char *user, + const char *password, size_t password_length); + +int BongoSaslInitialize(void); +void BongoSaslShutdown(void); +int BongoSaslSessionCreate(BongoSaslSession **session, const char *service, + const char *hostname, const char *local_address, + const char *remote_address, int tls_active, + BongoSaslVerify verify, void *context); +void BongoSaslSessionDestroy(BongoSaslSession **session); +int BongoSaslMechanisms(BongoSaslSession *session, const char **mechanisms); +int BongoSaslStart(BongoSaslSession *session, const char *mechanism, + const void *input, size_t input_length, + const void **output, size_t *output_length); +int BongoSaslStep(BongoSaslSession *session, const void *input, + size_t input_length, const void **output, + size_t *output_length); +const char *BongoSaslUsername(BongoSaslSession *session); +int BongoSaslDecode64(const char *input, size_t input_length, void *output, + size_t output_capacity, size_t *output_length); +int BongoSaslEncode64(const void *input, size_t input_length, char *output, + size_t output_capacity, size_t *output_length); + +enum { BONGO_SASL_FAIL = -1, BONGO_SASL_COMPLETE = 0, + BONGO_SASL_CONTINUE = 1 }; + +#ifdef __cplusplus +} +#endif +#endif diff --git a/include/bongosieve.h b/include/bongosieve.h new file mode 100644 index 0000000..ff112ea --- /dev/null +++ b/include/bongosieve.h @@ -0,0 +1,98 @@ +#ifndef BONGO_SIEVE_H +#define BONGO_SIEVE_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* Validate one complete UTF-8 RFC 5228 script without executing it. */ +int BongoSieveValidate(const char *script, size_t length); + +typedef enum { + BONGO_SIEVE_KEEP, + BONGO_SIEVE_DISCARD, + BONGO_SIEVE_FILEINTO, + BONGO_SIEVE_REDIRECT, + BONGO_SIEVE_REJECT, + BONGO_SIEVE_VACATION +} BongoSieveAction; +typedef struct { + BongoSieveAction action; + const char *argument; + const char *subject; + const char *from; + const char *handle; + const char *addresses[32]; + size_t address_count; + unsigned long days; + unsigned long seconds; + int has_seconds; + int mime; +} BongoSieveResult; +typedef int (*BongoSieveActionCallback)(const BongoSieveResult *result, + void *data); +/* Evaluate a script without allowing Mailutils to deliver or send anything. + * Each resulting action is reported to Bongo through callback. */ +int BongoSieveEvaluate(const char *script, size_t script_length, + const char *message, size_t message_length, + BongoSieveActionCallback callback, void *data); + +typedef struct BongoSieveStore BongoSieveStore; + +int BongoSieveStoreOpen(BongoSieveStore **store, const char *path); +void BongoSieveStoreClose(BongoSieveStore **store); +int BongoSieveStorePut(BongoSieveStore *store, const char *user, + const char *name, const char *script, size_t length); +int BongoSieveStoreGet(BongoSieveStore *store, const char *user, + const char *name, char **script, size_t *length, + int *active); +int BongoSieveStoreSetActive(BongoSieveStore *store, const char *user, + const char *name); +int BongoSieveStoreDelete(BongoSieveStore *store, const char *user, + const char *name); +int BongoSieveStoreRename(BongoSieveStore *store, const char *user, + const char *old_name, const char *new_name); +int BongoSieveVacationClaim(BongoSieveStore *store, const char *user, + const char *sender, const char *handle, + unsigned long interval_seconds); +void BongoSieveVacationRelease(BongoSieveStore *store, const char *user, + const char *sender, const char *handle); +typedef int (*BongoSieveListCallback)(const char *name, int active, void *data); +int BongoSieveStoreList(BongoSieveStore *store, const char *user, + BongoSieveListCallback callback, void *data); + +typedef enum { + BONGO_MANAGESIEVE_INVALID = 0, + BONGO_MANAGESIEVE_AUTHENTICATE, + BONGO_MANAGESIEVE_CAPABILITY, + BONGO_MANAGESIEVE_CHECKSCRIPT, + BONGO_MANAGESIEVE_DELETESCRIPT, + BONGO_MANAGESIEVE_GETSCRIPT, + BONGO_MANAGESIEVE_HAVESPACE, + BONGO_MANAGESIEVE_LISTSCRIPTS, + BONGO_MANAGESIEVE_LOGOUT, + BONGO_MANAGESIEVE_NOOP, + BONGO_MANAGESIEVE_PUTSCRIPT, + BONGO_MANAGESIEVE_RENAMESCRIPT, + BONGO_MANAGESIEVE_SETACTIVE, + BONGO_MANAGESIEVE_STARTTLS +} BongoManageSieveCommand; + +typedef struct { + BongoManageSieveCommand command; + char arguments[3][1025]; + size_t argument_count; + size_t literal_length; + int literal_nonsynchronizing; +} BongoManageSieveRequest; + +int BongoManageSieveParse(const char *line, size_t length, + BongoManageSieveRequest *request); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/include/bongoutil.h b/include/bongoutil.h index d8529f6..a21da1d 100644 --- a/include/bongoutil.h +++ b/include/bongoutil.h @@ -73,8 +73,8 @@ enum CommandColors { typedef int (* CommandHandler)(void *param); typedef struct _ProtocolCommand { - unsigned char *name; - unsigned char *help; + const char *name; + const char *help; int length; @@ -96,7 +96,7 @@ typedef struct _ProtocolCommandTree { ProtocolCommand proxy; } ProtocolCommandTree; -extern const unsigned char *Base64Chars; +extern const char Base64Chars[]; /***** Begin BongoKeyword *****/ #define BONGO_KEYWORD_VALUES_PER_BYTE 256 @@ -118,7 +118,7 @@ extern const unsigned char *Base64Chars; typedef unsigned long BongoKeywordTable[BONGO_KEYWORD_VALUES_PER_BYTE]; typedef struct { - char *string; + const char *string; unsigned long len; unsigned long id; } BongoKeyword; @@ -131,22 +131,22 @@ typedef struct { BOOL caseInsensitive; } BongoKeywordIndex; -BongoKeywordIndex *BongoKeywordIndexCreate(char **strings, unsigned long count, BOOL makeCaseInsensitive); +BongoKeywordIndex *BongoKeywordIndexCreate(const char *const *strings, unsigned long count, BOOL makeCaseInsensitive); void BongoKeywordIndexFree(BongoKeywordIndex *Index); -long BongoKeywordFind(BongoKeywordIndex *keywordTable, unsigned char *searchString); -long BongoKeywordBegins(BongoKeywordIndex *keywordIndex, unsigned char *searchString); +long BongoKeywordFind(BongoKeywordIndex *keywordTable, const char *searchString); +long BongoKeywordBegins(BongoKeywordIndex *keywordIndex, const char *searchString); BongoKeywordIndex *BongoKeywordIndexCreateProtocolCommands(ProtocolCommand *ProtocolCommands); #define BongoKeywordIndexCreateFromTable(INDEX, TABLE, COLUMN, CASEINSENSITIVE) \ { \ - char **strings; \ + const char **strings; \ unsigned long count; \ unsigned long row; \ count = 0; \ while(TABLE[count]COLUMN != NULL) { \ count++; \ } \ - strings = MemMalloc(sizeof(unsigned char *) * (count + 1)); \ + strings = MemMalloc(sizeof(*strings) * (count + 1)); \ if (strings) { \ for (row = 0; row < count; row++) { \ strings[row] = TABLE[row]COLUMN; \ @@ -165,7 +165,7 @@ unsigned long *QuickSortIndexCreate(char **stringList, unsigned long count, BOOL void QuickSortIndexFree(unsigned long *index); void LoadProtocolCommandTree(ProtocolCommandTree *tree, ProtocolCommand *commands); -ProtocolCommand *ProtocolCommandTreeSearch(ProtocolCommandTree *tree, const unsigned char *command); +ProtocolCommand *ProtocolCommandTreeSearch(ProtocolCommandTree *tree, const char *command); void RemoveLineEnding(char *s); BOOL QuickNCmp(char *str1, char *str2, int len); @@ -173,10 +173,10 @@ BOOL QuickCmp(char *str1, char *str2); #define BongoStrNCpy(dest, src, len) {strncpy((dest), (src), (len)); (dest)[(len) - 1] = '\0';} -unsigned char *DecodeBase64(unsigned char *EncodedString); -unsigned char *EncodeBase64(const unsigned char *UnencodedString); +char *DecodeBase64(char *EncodedString); +char *EncodeBase64(const char *UnencodedString); -BOOL HashCredential(unsigned char *Credential, unsigned char *Hash); +BOOL HashCredential(const char *Credential, char *Hash); /* String functions */ diff --git a/include/connio.h b/include/connio.h index 80615b2..52cee3e 100644 --- a/include/connio.h +++ b/include/connio.h @@ -41,6 +41,7 @@ typedef gnutls_certificate_credentials gnutls_certificate_credentials_t; typedef struct { gnutls_certificate_credentials_t cert_cred; + unsigned long options; } bongo_ssl_context; #define CONN_BUFSIZE 1023 @@ -78,6 +79,7 @@ typedef struct { #define SSL_ALLOW_CHAIN (1 << 3) #define SSL_DISABLE_EMPTY_FRAGMENTS (1 << 4) #define SSL_DONT_INSERT_EMPTY_FRAGMENTS (1 << 5) +#define SSL_ALLOW_LEGACY_PROTOCOLS (1 << 6) void CHOP_NEWLINE(char *s); #if 0 @@ -271,7 +273,7 @@ int ConnSend(Connection *Conn, char *Buffer, unsigned int Length); int ConnReceive(Connection *Conn, char *Buffer, unsigned int Length); int ConnRead(Connection *Conn, char *Dest, int Length); -int ConnReadCount(Connection *Conn, char *Dest, int Count); +int ConnReadCount(Connection *Conn, void *Dest, int Count); int ConnReadLine(Connection *Conn, char *Line, int Length); int ConnReadAnswer(Connection *Conn, char *Line, int Length); long ConnReadToAllocatedBuffer(Connection *c, char **buffer, unsigned long *bufferSize); @@ -280,7 +282,7 @@ int ConnReadToFileUntilEOS(Connection *Src, FILE *Dest); int ConnReadToConn(Connection *Src, Connection *Dest, int Count); int ConnReadToConnUntilEOS(Connection *Src, Connection *Dest); -int ConnWrite(Connection *Conn, const char *Source, int Count); +int ConnWrite(Connection *Conn, const void *Source, int Count); int ConnWriteF(Connection *Conn, const char *Format, ...) XPL_PRINTF(2, 3); int ConnWriteVF(Connection *c, const char *format, va_list ap); int ConnWriteFile(Connection *Conn, FILE *Source); diff --git a/include/memmgr.h b/include/memmgr.h index a475c58..5dd9044 100644 --- a/include/memmgr.h +++ b/include/memmgr.h @@ -75,7 +75,7 @@ XPL_BEGIN_C_LINKAGE /* Private Pool API's */ typedef BOOL (*PoolEntryCB)(void *Buffer, void *ClientData); -EXPORT void *MemPrivatePoolAlloc(unsigned char *Ident, size_t AllocationSize, unsigned int MinAllocCount, unsigned int MaxAllocCount, BOOL Dynamic, BOOL Temporary, PoolEntryCB AllocCB, PoolEntryCB FreeCB, void *ClientData); +EXPORT void *MemPrivatePoolAlloc(const char *Ident, size_t AllocationSize, unsigned int MinAllocCount, unsigned int MaxAllocCount, BOOL Dynamic, BOOL Temporary, PoolEntryCB AllocCB, PoolEntryCB FreeCB, void *ClientData); EXPORT void MemPrivatePoolFree(void *PoolHandle); EXPORT void *MemPrivatePoolGetEntryDirect(void *PoolHandle, const char *SourceFile, unsigned long SourceLine); diff --git a/include/msgaddr.h b/include/msgaddr.h index 8f497e4..4feb95c 100644 --- a/include/msgaddr.h +++ b/include/msgaddr.h @@ -109,7 +109,7 @@ EXPORT const unsigned char MsgAddressCharacters[]; -EXPORT BOOL MsgParseAddress(unsigned char *AddressLine, size_t AddressLineLength, unsigned char **local_part, unsigned char **domain); +EXPORT BOOL MsgParseAddress(char *AddressLine, size_t AddressLineLength, char **local_part, char **domain); EXPORT int MsgIsComment(unsigned char *Base, unsigned char *Limit, unsigned char **Out); EXPORT BOOL MsgIsXText(unsigned char *In, unsigned char *Limit, unsigned char **Out); @@ -127,7 +127,7 @@ IMPORT BOOL MsgIsXText(unsigned char *In, unsigned char *Limit, unsigned char ** extern const unsigned char MsgAddressCharacters[]; -BOOL MsgParseAddress(unsigned char *AddressLine, size_t AddressLineLength, unsigned char **local_part, unsigned char **domain); +BOOL MsgParseAddress(char *AddressLine, size_t AddressLineLength, char **local_part, char **domain); //BOOL MsgIsAddress(unsigned char *AddressLine, size_t AddressLineLength, unsigned char *Delimiters, unsigned char **LineOut); int MsgIsComment(unsigned char *Base, unsigned char *Limit, unsigned char **Out); BOOL MsgIsXText(unsigned char *In, unsigned char *Limit, unsigned char **Out); diff --git a/include/msgdate.h b/include/msgdate.h index 26fcdd1..124aa5c 100644 --- a/include/msgdate.h +++ b/include/msgdate.h @@ -109,11 +109,11 @@ #define TZ_DATELINE 74 typedef struct _MsgDateFormat { - unsigned char **monthShort; - unsigned char **monthLong; - unsigned char **wDayShort; - unsigned char **wDayLong; - unsigned char **AmPm; + const char *const *monthShort; + const char *const *monthLong; + const char *const *wDayShort; + const char *const *wDayLong; + const char *const *AmPm; unsigned long wDayStart; @@ -139,7 +139,7 @@ EXPORT void MsgGetDate(long rataDie, unsigned long *day, unsigned long *month, u EXPORT long MsgGetRataDie(unsigned long day, unsigned long month, unsigned long year); /* Date Interface */ -EXPORT BOOL MsgGetRFC822Date(long offset, long utcCime, unsigned char *dateBuffer); +EXPORT BOOL MsgGetRFC822Date(long offset, long utcCime, char *dateBuffer); EXPORT long MsgGetUTCOffset(void); EXPORT unsigned long MsgGetUTC(unsigned long day, unsigned long month, unsigned long year, unsigned long hour, unsigned long minute, unsigned long second); @@ -147,7 +147,7 @@ EXPORT BOOL MsgGetDMY(unsigned long utcTime, unsigned long *day, unsigned long * EXPORT unsigned long MsgGetTimezoneID(long day, long wDay, unsigned long month, unsigned long hour, long dstDay, long dstWDay, unsigned long dstMonth, unsigned long dstHour, long offset, long dstOffset); EXPORT long MsgGetUTCOffsetByDate(unsigned long timezone, unsigned long day, unsigned long month, unsigned long year, unsigned long hour); EXPORT long MsgGetUTCOffsetByUTC(unsigned long timezone, unsigned long utc); -EXPORT unsigned long MsgPrint(unsigned char *buffer, int bufLen, unsigned char *format, unsigned long time, MsgDateFormat *dateInfo); +EXPORT unsigned long MsgPrint(char *buffer, int bufLen, const char *format, unsigned long time, const MsgDateFormat *dateInfo); unsigned long MsgLookupRFC822Month(const char *name); uint64_t MsgParseRFC822DateTime(const char *date); diff --git a/include/rulesrv.h b/include/rulesrv.h index 6e3daeb..5ea9e5a 100644 --- a/include/rulesrv.h +++ b/include/rulesrv.h @@ -74,6 +74,8 @@ #define RULE_COND_HASMIMETYPE 'f' /* Arg1: Mime type to look for */ #define RULE_COND_HASMIMETYPENOT 'g' /* Arg1: Mime type to look for */ +#define RULE_COND_MAILINGLIST 'h' /* No args */ +#define RULE_COND_LISTID 'i' /* Arg1: RFC 2919 list identifier */ #define RULE_ACT_REPLY 'A' /* Arg1 : Filename */ #define RULE_ACT_DELETE 'B' /* No args */ diff --git a/include/streamio.h b/include/streamio.h index a39e5ad..e5a91d5 100644 --- a/include/streamio.h +++ b/include/streamio.h @@ -62,7 +62,7 @@ typedef struct _StreamStruct { typedef struct { - unsigned char *Charset; + const char *Charset; int (*Decoder)(struct _StreamStruct *Stream, struct _StreamStruct *NextStream); int (*Encoder)(struct _StreamStruct *Stream, struct _StreamStruct *NextStream); unsigned long CharsetType; @@ -73,9 +73,9 @@ typedef int (*StreamCodecFunc)(struct _StreamStruct *Stream, struct _StreamSt typedef BOOL (*TemplateHandlerFunc)(void *Client, void *Session, unsigned long TemplateID, void *Token, void *GotoToken, void *ObjectData); /* Helpers from streamio.c */ -StreamCodecFunc FindCodec(unsigned char *Charset, BOOL Encoder); -StreamCodecFunc FindCodecDecoder(unsigned char *Charset); -StreamCodecFunc FindCodecEncoder(unsigned char *Charset); +StreamCodecFunc FindCodec(const char *Charset, BOOL Encoder); +StreamCodecFunc FindCodecDecoder(const char *Charset); +StreamCodecFunc FindCodecEncoder(const char *Charset); BOOL StreamioInit(void); BOOL StreamioShutdown(void); @@ -86,4 +86,3 @@ extern StreamDescStruct StreamList[]; #endif #endif /* _STREAMIO_H_ */ - diff --git a/init/CMakeLists.txt b/init/CMakeLists.txt index 217f10c..3a7c58c 100644 --- a/init/CMakeLists.txt +++ b/init/CMakeLists.txt @@ -10,3 +10,26 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/bongo.xml.solaris-smf ${CMAKE_CURRENT #configure_file(${CMAKE_CURRENT_SOURCE_DIR}/http-bongo.solaris-smf.in ${CMAKE_CURRENT_BINARY_DIR}/http-bongo.solaris-smf) #configure_file(${CMAKE_CURRENT_SOURCE_DIR}/http-bongo.xml.solaris-smf ${CMAKE_CURRENT_BINARY_DIR}/http-bongo.xml.solaris-smf) configure_file(${CMAKE_CURRENT_SOURCE_DIR}/svc-bongo.solaris-smf.in ${CMAKE_CURRENT_BINARY_DIR}/svc-bongo.solaris-smf) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/bongo.service.in + ${CMAKE_CURRENT_BINARY_DIR}/bongo.service @ONLY) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/bongo.tmpfiles.in + ${CMAKE_CURRENT_BINARY_DIR}/bongo.conf @ONLY) +configure_file(${CMAKE_CURRENT_SOURCE_DIR}/bongo.sysusers.in + ${CMAKE_CURRENT_BINARY_DIR}/bongo-user.conf @ONLY) + +set(SYSTEMD_SYSTEM_UNIT_DIR "lib/systemd/system" CACHE PATH + "Directory for systemd system service units") +set(SYSTEMD_TMPFILES_DIR "lib/tmpfiles.d" CACHE PATH + "Directory for systemd tmpfiles configuration") +set(SYSTEMD_SYSUSERS_DIR "lib/sysusers.d" CACHE PATH + "Directory for systemd sysusers configuration") +option(INSTALL_SYSTEMD_UNITS "Install systemd service and tmpfiles files" ON) + +if(INSTALL_SYSTEMD_UNITS) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/bongo.service + DESTINATION ${SYSTEMD_SYSTEM_UNIT_DIR}) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/bongo.conf + DESTINATION ${SYSTEMD_TMPFILES_DIR}) + install(FILES ${CMAKE_CURRENT_BINARY_DIR}/bongo-user.conf + DESTINATION ${SYSTEMD_SYSUSERS_DIR}) +endif() diff --git a/init/bongo.service.in b/init/bongo.service.in new file mode 100644 index 0000000..e914761 --- /dev/null +++ b/init/bongo.service.in @@ -0,0 +1,27 @@ +[Unit] +Description=Bongo mail and calendar server +Documentation=https://gitea.disconnected-by-peer.at/geos_one/bongo +Wants=network-online.target +After=network-online.target nss-lookup.target local-fs.target clamd.service clamav-daemon.service spamassassin.service spamd.service + +[Service] +Type=simple +ExecStart=@SBIN_INSTALL_DIR@/bongo-manager -k +ExecReload=/bin/kill -USR1 $MAINPID +Restart=on-failure +RestartSec=5s +TimeoutStartSec=60s +TimeoutStopSec=45s +KillMode=control-group +KillSignal=SIGTERM +UMask=0077 +LimitNOFILE=65536 +TasksMax=4096 +PrivateTmp=true +PrivateDevices=true +ProtectHome=true +ProtectSystem=full +ReadWritePaths=@XPL_DEFAULT_STATE_DIR@ + +[Install] +WantedBy=multi-user.target diff --git a/init/bongo.sysusers.in b/init/bongo.sysusers.in new file mode 100644 index 0000000..42d276e --- /dev/null +++ b/init/bongo.sysusers.in @@ -0,0 +1 @@ +u @BONGO_USER@ - "Bongo mail and calendar server" @XPL_DEFAULT_STATE_DIR@ - diff --git a/init/bongo.tmpfiles.in b/init/bongo.tmpfiles.in new file mode 100644 index 0000000..8ed09db --- /dev/null +++ b/init/bongo.tmpfiles.in @@ -0,0 +1,9 @@ +d @XPL_DEFAULT_STATE_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ - +d @XPL_DEFAULT_DBF_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ - +d @XPL_DEFAULT_WORK_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ - +d @XPL_DEFAULT_CACHE_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ - +d @XPL_DEFAULT_SCMS_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ - +d @XPL_DEFAULT_SPOOL_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ - +d @XPL_DEFAULT_SYSTEM_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ - +d @XPL_DEFAULT_MAIL_DIR@ 0750 @BONGO_USER@ @BONGO_USER@ - +d @XPL_DEFAULT_COOKIE_DIR@ 0700 @BONGO_USER@ @BONGO_USER@ - diff --git a/po/bongo-web/de.po b/po/bongo-web/de.po new file mode 100644 index 0000000..55e08f5 --- /dev/null +++ b/po/bongo-web/de.po @@ -0,0 +1,105 @@ +msgid "" +msgstr "" +"Project-Id-Version: Bongo 0.7.0\n" +"PO-Revision-Date: 2026-07-15 00:00+0200\n" +"Last-Translator: Bongo Project\n" +"Language-Team: German\n" +"Language: de\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" + +msgid "Email address" +msgstr "E-Mail-Adresse" +msgid "Password" +msgstr "Passwort" +msgid "Sign in" +msgstr "Anmelden" +msgid "Sign out" +msgstr "Abmelden" +msgid "Mail" +msgstr "Mail" +msgid "Calendar" +msgstr "Kalender" +msgid "Contacts" +msgstr "Kontakte" +msgid "Administration" +msgstr "Administration" +msgid "Mail, calendar and contacts" +msgstr "Mail, Kalender und Kontakte" +msgid "Login failed." +msgstr "Anmeldung fehlgeschlagen." +msgid "Too many login attempts. Please try again later." +msgstr "Zu viele Anmeldeversuche. Bitte später erneut versuchen." +msgid "Calendar access is available through CalDAV." +msgstr "Der Kalenderzugriff ist über CalDAV verfügbar." +msgid "Contact access is available through CardDAV." +msgstr "Der Kontaktzugriff ist über CardDAV verfügbar." +msgid "The secure webmail connection is being enabled." +msgstr "Die sichere Webmail-Anbindung wird aktiviert." +msgid "Administrative functions require a server-side role check." +msgstr "Administrative Funktionen erfordern eine serverseitige Rollenprüfung." +msgid "Folder" +msgstr "Ordner" +msgid "Refresh" +msgstr "Aktualisieren" +msgid "(no subject)" +msgstr "(kein Betreff)" +msgid "Back" +msgstr "Zurück" +msgid "Load external images" +msgstr "Externe Bilder laden" +msgid "[external image blocked]" +msgstr "[externes Bild blockiert]" +msgid "Compose new mail" +msgstr "Neue E-Mail verfassen" +msgid "From" +msgstr "Von" +msgid "To" +msgstr "An" +msgid "Cc" +msgstr "Kopie" +msgid "Bcc" +msgstr "Blindkopie" +msgid "Subject" +msgstr "Betreff" +msgid "Message" +msgstr "Nachricht" +msgid "Send" +msgstr "Senden" +msgid "Cancel" +msgstr "Abbrechen" +msgid "Message sent." +msgstr "Nachricht wurde gesendet." +msgid "The message could not be sent." +msgstr "Die Nachricht konnte nicht gesendet werden." +msgid "Invalid message fields." +msgstr "Ungültige Nachrichtenfelder." +msgid "Tasks" +msgstr "Aufgaben" +msgid "No open tasks." +msgstr "Keine offenen Aufgaben." +msgid "Ignore" +msgstr "Ignorieren" +msgid "Disable" +msgstr "Deaktivieren" +msgid "Done" +msgstr "Erledigt" +msgid "Settings" +msgstr "Einstellungen" +msgid "Personal settings" +msgstr "Persönliche Einstellungen" +msgid "Open tasks" +msgstr "Offene Aufgaben" +msgid "Filters" +msgstr "Filter" +msgid "External mail accounts" +msgstr "Externe E-Mail-Konten" +msgid "Sender identities" +msgstr "Absenderidentitäten" +msgid "Two-factor authentication" +msgstr "Zwei-Faktor-Authentifizierung" +msgid "App passwords" +msgstr "App-Passwörter" +msgid "Language and display" +msgstr "Sprache und Darstellung" diff --git a/po/bongo-web/fr.po b/po/bongo-web/fr.po index 0841ad3..240dc6e 100644 --- a/po/bongo-web/fr.po +++ b/po/bongo-web/fr.po @@ -12,10 +12,58 @@ msgstr "" "PO-Revision-Date: 2007-06-30 17:48+1000\n" "Last-Translator: Alexander Hixon \n" "Language-Team: fr\n" +"Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=utf-8\n" "Content-Transfer-Encoding: 8bit\n" +msgid "Email address" +msgstr "Adresse e-mail" +msgid "Password" +msgstr "Mot de passe" +msgid "Sign in" +msgstr "Se connecter" +msgid "Sign out" +msgstr "Se déconnecter" +msgid "Contacts" +msgstr "Contacts" +msgid "Mail, calendar and contacts" +msgstr "Courrier, calendrier et contacts" +msgid "Login failed." +msgstr "Échec de la connexion." +msgid "Too many login attempts. Please try again later." +msgstr "Trop de tentatives de connexion. Veuillez réessayer plus tard." +msgid "Calendar access is available through CalDAV." +msgstr "L’accès au calendrier est disponible via CalDAV." +msgid "Contact access is available through CardDAV." +msgstr "L’accès aux contacts est disponible via CardDAV." +msgid "The secure webmail connection is being enabled." +msgstr "La connexion sécurisée au webmail est en cours d’activation." +msgid "Administrative functions require a server-side role check." +msgstr "Les fonctions administratives nécessitent une vérification du rôle côté serveur." +msgid "Folder" +msgstr "Dossier" +msgid "Refresh" +msgstr "Actualiser" +msgid "(no subject)" +msgstr "(sans objet)" +msgid "Back" +msgstr "Retour" +msgid "Load external images" +msgstr "Charger les images externes" +msgid "[external image blocked]" +msgstr "[image externe bloquée]" +msgid "Cc" +msgstr "Cc" +msgid "Bcc" +msgstr "Cci" +msgid "Subject" +msgstr "Objet" +msgid "The message could not be sent." +msgstr "Le message n’a pas pu être envoyé." +msgid "Invalid message fields." +msgstr "Champs du message non valides." + #: src/www/js/AddressBook.js:232 #, fuzzy msgid "Add Contact" @@ -70,9 +118,8 @@ msgstr "" #: src/www/js/OccurrenceEditor.js:137 src/www/js/OccurrenceEditor.js:143 #: src/www/js/OccurrenceEditor.js:262 src/www/js/Widgets.js:1079 #: src/www/js/Widgets.js:1126 -#, fuzzy msgid "Cancel" -msgstr "Annulation" +msgstr "Annuler" #: src/www/js/AddressBook.js:608 src/www/js/CalendarEditor.js:915 #: src/www/js/OccurrenceEditor.js:138 src/www/js/OccurrenceEditor.js:144 @@ -380,9 +427,8 @@ msgid "Save Draft" msgstr "Save draft" #: src/www/js/Composer.js:163 -#, fuzzy msgid "Send" -msgstr "Send" +msgstr "Envoyer" #: src/www/js/Composer.js:321 msgid "Untitled" @@ -402,7 +448,7 @@ msgstr "" #: src/www/js/Composer.js:459 msgid "Message sent." -msgstr "" +msgstr "Message envoyé." #: src/www/js/Composer.js:471 msgid "Error sending message: " @@ -456,7 +502,7 @@ msgstr "" #: src/www/js/Dragonfly.js:906 msgid "Compose new mail" -msgstr "" +msgstr "Nouveau message" #: src/www/js/Dragonfly.js:908 src/www/js/Dragonfly.js:909 #, fuzzy @@ -627,12 +673,11 @@ msgstr "Item" #: src/www/js/Mail.js:662 msgid "To" -msgstr "" +msgstr "À" #: src/www/js/Mail.js:662 -#, fuzzy msgid "From" -msgstr "From:" +msgstr "De" #: src/www/js/MailListView.js:101 src/www/js/MailListView.js:182 msgid "No messages here." @@ -1331,6 +1376,45 @@ msgstr "" #~ msgid "mailSubscriptionsLabel" #~ msgstr "Mailing Lists" +msgid "Tasks" +msgstr "Tâches" + +msgid "No open tasks." +msgstr "Aucune tâche ouverte." + +msgid "Disable" +msgstr "Désactiver" + +msgid "Done" +msgstr "Terminé" + +msgid "Settings" +msgstr "Paramètres" + +msgid "Personal settings" +msgstr "Paramètres personnels" + +msgid "Open tasks" +msgstr "Tâches ouvertes" + +msgid "Filters" +msgstr "Filtres" + +msgid "External mail accounts" +msgstr "Comptes de messagerie externes" + +msgid "Sender identities" +msgstr "Identités d’expéditeur" + +msgid "Two-factor authentication" +msgstr "Authentification à deux facteurs" + +msgid "App passwords" +msgstr "Mots de passe d’application" + +msgid "Language and display" +msgstr "Langue et affichage" + #~ msgid "mailNoMessages" #~ msgstr "No messages found." diff --git a/po/bongo-web/pt_BR.po b/po/bongo-web/pt_BR.po index e2f3c0b..1fe1fb5 100644 --- a/po/bongo-web/pt_BR.po +++ b/po/bongo-web/pt_BR.po @@ -3,19 +3,66 @@ # This file is distributed under the same license as the Bongo package. # Igor Morgado , 2007 # -#, fuzzy msgid "" msgstr "" -"Project-Id-Version: PACKAGE VERSION\n" -"Report-Msgid-Bugs-To: bongo-devel@gna.org\n" +"Project-Id-Version: Bongo 0.7.0\n" +"Report-Msgid-Bugs-To: bongo-devel@disconnected-by-peer.at\n" "POT-Creation-Date: 2007-07-13 11:46+0100\n" "PO-Revision-Date: 2007-11-08 17:47+BRST\n" "Last-Translator: Igor Morgado \n" "Language-Team: Portuguese\n" "MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=pt_BR.UTF-8\n" +"Language: pt_BR\n" +"Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" +msgid "Email address" +msgstr "Endereço de e-mail" +msgid "Password" +msgstr "Senha" +msgid "Sign in" +msgstr "Entrar" +msgid "Sign out" +msgstr "Sair" +msgid "Contacts" +msgstr "Contatos" +msgid "Mail, calendar and contacts" +msgstr "E-mail, calendário e contatos" +msgid "Login failed." +msgstr "Falha no login." +msgid "Too many login attempts. Please try again later." +msgstr "Muitas tentativas de login. Tente novamente mais tarde." +msgid "Calendar access is available through CalDAV." +msgstr "O acesso ao calendário está disponível por CalDAV." +msgid "Contact access is available through CardDAV." +msgstr "O acesso aos contatos está disponível por CardDAV." +msgid "The secure webmail connection is being enabled." +msgstr "A conexão segura ao webmail está sendo ativada." +msgid "Administrative functions require a server-side role check." +msgstr "As funções administrativas exigem verificação de função no servidor." +msgid "Folder" +msgstr "Pasta" +msgid "Refresh" +msgstr "Atualizar" +msgid "(no subject)" +msgstr "(sem assunto)" +msgid "Back" +msgstr "Voltar" +msgid "Load external images" +msgstr "Carregar imagens externas" +msgid "[external image blocked]" +msgstr "[imagem externa bloqueada]" +msgid "Cc" +msgstr "Cc" +msgid "Bcc" +msgstr "Cco" +msgid "Subject" +msgstr "Assunto" +msgid "The message could not be sent." +msgstr "Não foi possível enviar a mensagem." +msgid "Invalid message fields." +msgstr "Campos da mensagem inválidos." + #: src/www/js/AddressBook.js:232 msgid "Add Contact" msgstr "Adicionar contato" @@ -870,3 +917,42 @@ msgstr "Adicionar como padrão" #: src/www/js/Widgets.js:1131 msgid "Set" msgstr "Configurar" + +msgid "Tasks" +msgstr "Tarefas" + +msgid "No open tasks." +msgstr "Nenhuma tarefa aberta." + +msgid "Disable" +msgstr "Desativar" + +msgid "Done" +msgstr "Concluído" + +msgid "Settings" +msgstr "Configurações" + +msgid "Personal settings" +msgstr "Configurações pessoais" + +msgid "Open tasks" +msgstr "Tarefas abertas" + +msgid "Filters" +msgstr "Filtros" + +msgid "External mail accounts" +msgstr "Contas de e-mail externas" + +msgid "Sender identities" +msgstr "Identidades do remetente" + +msgid "Two-factor authentication" +msgstr "Autenticação de dois fatores" + +msgid "App passwords" +msgstr "Senhas de aplicativos" + +msgid "Language and display" +msgstr "Idioma e aparência" diff --git a/src/agents/antispam/antispam.c b/src/agents/antispam/antispam.c index 2037fbf..9e9770a 100644 --- a/src/agents/antispam/antispam.c +++ b/src/agents/antispam/antispam.c @@ -125,7 +125,8 @@ ProcessConnection(void *clientp, Connection *conn) BONGO_ENVELOPE_NEXT(envelopeLine); } - SpamdCheck(&ASpam.spamd, client, client->qID, hasFlags, msgFlags); + if (ASpam.spamd.enabled) + SpamdCheck(&ASpam.spamd, client, client->qID, hasFlags, msgFlags); //blocked = SpamdCheck(&ASpam.spamd, client, client->qID, hasFlags, msgFlags); done: diff --git a/src/agents/antispam/antispam.h b/src/agents/antispam/antispam.h index 555da6a..f14ecdd 100644 --- a/src/agents/antispam/antispam.h +++ b/src/agents/antispam/antispam.h @@ -85,6 +85,8 @@ typedef struct { GArray *hostlist; BOOL enabled; unsigned long connectionTimeout; + unsigned long maximumMessageSize; + unsigned long maximumResponseSize; } SpamdConfig; typedef struct _ASpamGlobals { diff --git a/src/agents/antispam/spamd.c b/src/agents/antispam/spamd.c index 05b4186..029e204 100644 --- a/src/agents/antispam/spamd.c +++ b/src/agents/antispam/spamd.c @@ -68,6 +68,7 @@ SpamdCheck(SpamdConfig *spamd, ASpamClient *client, const char *queueID, BOOL ha int ccode; BOOL infected; BOOL success; + BOOL spam = FALSE; double score = 0; Connection *conn; /* Holds the current connection to spamd. */ @@ -95,6 +96,11 @@ SpamdCheck(SpamdConfig *spamd, ASpamClient *client, const char *queueID, BOOL ha size = atol(client->line); } + if (size > spamd->maximumMessageSize) { + Log(LOG_NOTICE, "(%s) Message size %lu exceeds spam scan limit %lu; delivering unscanned", + queueID, size, spamd->maximumMessageSize); + goto ErrConnFree; + } if ( (size != 0) && ((ccode = ConnWrite(conn, "PROCESS SPAMC/1.3\r\n", 19) ) != -1) && ((ccode = ConnWriteF(conn, "Content-length: %ld\r\n\r\n", size)) != -1) @@ -153,18 +159,24 @@ SpamdCheck(SpamdConfig *spamd, ASpamClient *client, const char *queueID, BOOL ha */ ptr = client->line + strlen("Content-length: "); size = atol(ptr); - if (size == 0) { + if (size == 0 || size > spamd->maximumResponseSize) { + Log(LOG_ERROR, "(%s) Invalid or oversized spamd response (%lu bytes)", + queueID, size); goto ErrConnFree; } } else if (strncmp(client->line, "Spam: ", strlen("Spam: ")) == 0) { /* Looking for the spam score. */ + spam = !strncasecmp(client->line + strlen("Spam: "), "True", 4); ptr = strchr(client->line, ';'); - score = atof(++ptr); - Log(LOG_INFO, "(%s) spam score of %f", queueID, score); + if (!ptr || !*++ptr) goto ErrConnFree; + score = strtod(ptr, &ptr2); + if (ptr2 == ptr) goto ErrConnFree; + Log(LOG_INFO, "(%s) spam=%s score=%.3f", queueID, + spam ? "yes" : "no", score); } } - infected = TRUE; + infected = spam; /* Create a new message with spam headers added before deleting this one. * * We will use the envelope that we read out in the beginning @@ -324,7 +336,10 @@ static BongoConfigItem SpamdHostList = { BONGO_JSON_STRING, NULL, &ASpam.spamd.h // ASpam.nmap.queue - defaults to Q_INCOMING static BongoConfigItem SpamdConfigSchema[] = { + { BONGO_JSON_BOOL, "o:enabled/b", &ASpam.spamd.enabled }, { BONGO_JSON_INT, "o:timeout/i", &ASpam.spamd.connectionTimeout }, + { BONGO_JSON_INT, "o:maximum_message_size/i", &ASpam.spamd.maximumMessageSize }, + { BONGO_JSON_INT, "o:maximum_response_size/i", &ASpam.spamd.maximumResponseSize }, { BONGO_JSON_ARRAY, "o:hosts/a", &SpamdHostList}, { BONGO_JSON_NULL, NULL, NULL }, }; @@ -338,11 +353,16 @@ SpamdReadConfiguration(SpamdConfig *spamd) /* Set up default spamd options. */ spamd->connectionTimeout = SPAMD_DEFAULT_CONNECTION_TIMEOUT; + spamd->enabled = TRUE; + spamd->maximumMessageSize = 50UL * 1024UL * 1024UL; + spamd->maximumResponseSize = 51UL * 1024UL * 1024UL; ConnAddressPoolStartup(&spamd->hosts, 0, 0); if (!ReadBongoConfiguration(SpamdConfigSchema, "antispam")) return FALSE; + if (!spamd->maximumMessageSize || + spamd->maximumResponseSize < spamd->maximumMessageSize) return FALSE; for (i=0; i < ASpam.spamd.hostlist->len; i++) { char *hostitem = g_array_index(ASpam.spamd.hostlist, char*, i); char *lHost = MemStrdup(hostitem); diff --git a/src/agents/avirus/avirus.c b/src/agents/avirus/avirus.c index b295a1e..763fdb5 100755 --- a/src/agents/avirus/avirus.c +++ b/src/agents/avirus/avirus.c @@ -59,93 +59,86 @@ AVirusClientFree(void *client) return; } -BOOL VirusCheck(AVirusClient *client, const char *queueID, BOOL hasFlags, unsigned long msgFlags, unsigned long senderIp, char *senderUserName) { +#define CLAMD_INSTREAM_CHUNK_SIZE 8192 + +AVirusScanResult VirusCheck(AVirusClient *client, const char *queueID, BOOL hasFlags, unsigned long msgFlags, unsigned long senderIp, char *senderUserName) { int ccode; long size; - BOOL infected = FALSE; + long remaining; + char chunk[CLAMD_INSTREAM_CHUNK_SIZE]; Connection *conn; + UNUSED_PARAMETER_REFACTOR(hasFlags); + UNUSED_PARAMETER_REFACTOR(msgFlags); + UNUSED_PARAMETER_REFACTOR(senderIp); + UNUSED_PARAMETER_REFACTOR(senderUserName); + conn = ConnAddressPoolConnect(&(AVirus.clamd.hosts), AVirus.clamd.connectionTimeout); - if (conn) { - Connection *data; - unsigned short port; - - ConnWrite(conn, "STREAM\r\n", strlen("STREAM\r\n")); - ConnFlush(conn); - ccode = ConnReadAnswer(conn, client->line, CONN_BUFSIZE); - - if (!ccode || strncmp(client->line, "PORT ", strlen("PORT ")) != 0 || (port = atoi(client->line + strlen("PORT "))) == 0) { - ConnFree(conn); - return -1; - } - - data = ConnAlloc(TRUE); - if (!data) { - ConnFree(conn); - return -1; - } - - memcpy(&data->socketAddress, &conn->socketAddress, sizeof(struct sockaddr_in)); - data->socketAddress.sin_port = htons(port); - if (ConnConnectEx(data, NULL, 0, NULL, client->conn->trace.destination) < 0) { - ConnFree(conn); - ConnFree(data); - return -1; - } - - size = 0; - if (((ccode = NMAPSendCommandF(client->conn, "QRETR %s MESSAGE\r\n", queueID)) != -1) - && ((ccode = NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, TRUE)) != -1) - && ccode == 2023) { - char *ptr; - - ptr = strchr (client->line, ' '); - if (ptr) { - *ptr = '\0'; - } - - size = atol(client->line); - } - - if (size == 0) { - ConnFree(conn); - ConnFree(data); - return -1; - } - - ccode = ConnReadToConn(client->conn, data, size); - - ConnFree(data); - - if ((ccode == -1) || ((ccode = NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, TRUE)) != 1000)) { - Log(LOG_DEBUG, "result: %d", ccode); - ConnFree(conn); - return -1; - } - - while ((ccode = ConnReadAnswer(conn, client->line, CONN_BUFSIZE)) != -1) { - char *ptr; - - ptr = strrchr(client->line, ' '); - if (XplStrCaseCmp(ptr + 1, "FOUND") == 0) { - *ptr = '\0'; - ptr = client->line + strlen("stream: "); - -#if 0 - if(client->foundViruses.used == client->foundViruses.allocated) { - client->foundViruses.names = MemRealloc(client->foundViruses.names, sizeof(char*) * (client->foundViruses.allocated + MIME_REALLOC_SIZE)); - } - client->foundViruses.names[client->foundViruses.used++] = MemStrdup(ptr); - - XplSafeIncrement(AVirus.stats.viruses); -#endif - infected = TRUE; - } - } - ConnFree(conn); + if (!conn) { + return AVIRUS_SCAN_ERROR; } - return infected; + size = 0; + if (((ccode = NMAPSendCommandF(client->conn, "QRETR %s MESSAGE\r\n", queueID)) == -1) + || ((ccode = NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, TRUE)) != 2023)) { + ConnFree(conn); + return AVIRUS_SCAN_ERROR; + } + { + char *end = NULL; + size = strtol(client->line, &end, 10); + if (end == client->line || size <= 0) { + ConnFree(conn); + return AVIRUS_SCAN_ERROR; + } + } + + if (ConnWrite(conn, "nINSTREAM\n", sizeof("nINSTREAM\n") - 1) == -1) { + ConnFree(conn); + return AVIRUS_SCAN_ERROR; + } + + remaining = size; + while (remaining > 0) { + int count = remaining > (long)sizeof(chunk) ? (int)sizeof(chunk) : (int)remaining; + uint32_t networkCount = htonl((uint32_t)count); + + if (ConnReadCount(client->conn, chunk, count) != count + || ConnWrite(conn, (const char *)&networkCount, sizeof(networkCount)) == -1 + || ConnWrite(conn, chunk, count) == -1) { + ConnFree(conn); + return AVIRUS_SCAN_ERROR; + } + remaining -= count; + } + { + uint32_t endMarker = 0; + if (ConnWrite(conn, (const char *)&endMarker, sizeof(endMarker)) == -1 + || ConnFlush(conn) == -1) { + ConnFree(conn); + return AVIRUS_SCAN_ERROR; + } + } + + if (NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, TRUE) != 1000) { + ConnFree(conn); + return AVIRUS_SCAN_ERROR; + } + + ccode = ConnReadAnswer(conn, client->line, CONN_BUFSIZE); + ConnFree(conn); + if (ccode <= 0) { + return AVIRUS_SCAN_ERROR; + } + if (strstr(client->line, " FOUND") != NULL) { + return AVIRUS_SCAN_INFECTED; + } + if (strstr(client->line, " OK") != NULL) { + return AVIRUS_SCAN_CLEAN; + } + + Log(LOG_ERROR, "ClamAV scan failed for queue %s: %s", queueID, client->line); + return AVIRUS_SCAN_ERROR; } void BounceToPostmaster(AVirusClient *client, char *senderUserName) { @@ -268,16 +261,16 @@ static __inline int ProcessConnection(void *clientp, Connection *conn) { AVirusClient *client = clientp; - unsigned char *envelopeLine; + char *envelopeLine; BOOL hasFlags = FALSE; unsigned long msgFlags = 0; int ccode; unsigned long source = 0; - unsigned char *ptr; + char *ptr; char *ptr2; char *senderUserName = NULL; - BOOL blocked = FALSE; + AVirusScanResult scanResult; client->conn = conn; ccode = BongoQueueAgentHandshake(client->conn, client->line, client->qID, &client->envelopeLength, &client->messageLength); @@ -309,9 +302,9 @@ ProcessConnection(void *clientp, Connection *conn) BONGO_ENVELOPE_NEXT(envelopeLine); } - blocked = VirusCheck(client, client->qID, hasFlags, msgFlags, source, senderUserName); + scanResult = VirusCheck(client, client->qID, hasFlags, msgFlags, source, senderUserName); - if (blocked == TRUE) { + if (scanResult == AVIRUS_SCAN_INFECTED) { /* we found a virus of some kind. drop the mail */ Log(LOG_DEBUG, "Queue ID %s Infected and blocked", client->qID); ConnWriteF(client->conn, "QDELE %s\r\n", client->qID); @@ -330,6 +323,11 @@ ProcessConnection(void *clientp, Connection *conn) /* create the bounce to the postmaster */ BounceToPostmaster(client, senderUserName); } + } else if (scanResult == AVIRUS_SCAN_ERROR) { + /* Do not acknowledge an unscanned message. Closing the queue-agent + * transaction leaves it queued so a later worker can retry it. */ + Log(LOG_ERROR, "ClamAV could not scan queue ID %s; message remains queued for retry", client->qID); + ccode = -1; } else { Log(LOG_DEBUG, "Queue ID %s clean", client->qID); NMAPSendCommand(client->conn, "QDONE\r\n", 7); @@ -345,7 +343,7 @@ ProcessConnection(void *clientp, Connection *conn) MemFree(senderUserName); } - return(0); + return ccode == -1 ? -1 : 0; } static BongoConfigItem ClamHostList = { @@ -424,7 +422,7 @@ ReadConfiguration(void) { for (i=0; i < AVirus.clamd.hostlist->len; i++) { char *hostitem = g_array_index(AVirus.clamd.hostlist, char*, i); char *lHost = MemStrdup(hostitem); - char *host; + char *host = NULL; int port=AVIRUS_DEFAULT_PORT, weight=AVIRUS_DEFAULT_WEIGHT; ParseHost(lHost, &host, &port, &weight); ConnAddressPoolAddHost(&AVirus.clamd.hosts, host, port, weight); @@ -493,6 +491,7 @@ XplServiceCode(SignalHandler) static void AntiVirusServer(void *ignored) { + UNUSED_PARAMETER(ignored); int minThreads, maxThreads, minSleep; BongoQueueAgentGetThreadPoolParameters(&AVirus.agent, &minThreads, &maxThreads, &minSleep); @@ -514,6 +513,8 @@ AntiVirusServer(void *ignored) { int XplServiceMain(int argc, char *argv[]) { + UNUSED_PARAMETER(argc); + UNUSED_PARAMETER(argv); int ccode; int startupOpts; diff --git a/src/agents/avirus/avirus.h b/src/agents/avirus/avirus.h index ac8b097..2febded 100755 --- a/src/agents/avirus/avirus.h +++ b/src/agents/avirus/avirus.h @@ -65,19 +65,25 @@ typedef enum _AVirusClientFlags { AVIRUS_CLIENT_FLAG_EXITING = (1 << 2) } AVirusClientFlags; +typedef enum { + AVIRUS_SCAN_ERROR = -1, + AVIRUS_SCAN_CLEAN = 0, + AVIRUS_SCAN_INFECTED = 1 +} AVirusScanResult; + typedef struct { AVirusClientFlags flags; Connection *conn; - unsigned int envelopeLength; - unsigned int messageLength; - unsigned int envelopeLines; + int envelopeLength; + int messageLength; + int envelopeLines; char qID[16]; /* holds the queueID */ - unsigned char *envelope; - unsigned char line[CONN_BUFSIZE + 1]; - unsigned char command[CONN_BUFSIZE + 1]; + char *envelope; + char line[CONN_BUFSIZE + 1]; + char command[CONN_BUFSIZE + 1]; } AVirusClient; @@ -132,7 +138,7 @@ extern AVirusGlobals AVirus; /* spamd.c */ void BounceToSender(AVirusClient *client, char *senderUserName); void BounceToPostmaster(AVirusClient *client, char *senderUserName); -BOOL VirusCheck(AVirusClient *client, const char *queueID, BOOL hasFlags, unsigned long msgFlags, unsigned long senderIp, char *senderUserName); +AVirusScanResult VirusCheck(AVirusClient *client, const char *queueID, BOOL hasFlags, unsigned long msgFlags, unsigned long senderIp, char *senderUserName); void VirusShutdown(); void VirusStartup(); BOOL VirusReadConfiguration(BongoJsonNode *node); diff --git a/src/agents/collector/CMakeLists.txt b/src/agents/collector/CMakeLists.txt new file mode 100644 index 0000000..e180ab1 --- /dev/null +++ b/src/agents/collector/CMakeLists.txt @@ -0,0 +1,32 @@ +StrictCompile() + +set(EXTERNAL_ACCOUNTS_SQL_C ${CMAKE_CURRENT_BINARY_DIR}/external-accounts-sql.c) +add_custom_command( + OUTPUT ${EXTERNAL_ACCOUNTS_SQL_C} + COMMAND ${CMAKE_COMMAND} + -DINPUT=${CMAKE_CURRENT_SOURCE_DIR}/external-accounts.sql + -DOUTPUT=${EXTERNAL_ACCOUNTS_SQL_C} + -DSYMBOL=sql_external_accounts + -P ${CMAKE_SOURCE_DIR}/cmake/EmbedFile.cmake + DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/external-accounts.sql + ${CMAKE_SOURCE_DIR}/cmake/EmbedFile.cmake + VERBATIM +) + +add_executable(bongocollector + collector.c + external_accounts.c + ${EXTERNAL_ACCOUNTS_SQL_C} +) + +target_link_libraries(bongocollector + bongoxpl + bongoconnio + bongoutil + bongostreamio + bongomsgapi + ${CURL_LIBRARIES} + ${SQLITE_LIBRARIES} +) + +install(TARGETS bongocollector DESTINATION ${SBIN_INSTALL_DIR}) diff --git a/src/agents/collector/collector.c b/src/agents/collector/collector.c index f1a4ac4..0468711 100644 --- a/src/agents/collector/collector.c +++ b/src/agents/collector/collector.c @@ -35,27 +35,48 @@ #include #include "collector.h" +#include "external_accounts.h" #define TOK_ARR_SZ 10 -CollectorGlobals Collector = {{0,}, 0, }; +CollectorGlobals Collector = {0}; static void CollectorLoop(void *ignored) { + sqlite3 *accounts = NULL; + time_t lastCalendarCollection = 0; + + (void) ignored; XplRenameThread(XplGetThreadID(), AGENT_DN); while (Collector.agent.state < BONGO_AGENT_STATE_STOPPING) { - MsgCollectAllUsers(); + time_t now = time(NULL); + int due; + + if (accounts == NULL) accounts = CollectorAccountsOpen(); + if (accounts != NULL) { + due = CollectorAccountsDueCount(accounts, (long) now); + if (due < 0) { + Log(LOG_ERROR, "Unable to query due external mail accounts"); + } else if (due > 0) { + Log(LOG_DEBUG, "%d external mail account(s) are due for collection", due); + } + } + if (lastCalendarCollection == 0 || now - lastCalendarCollection >= 6 * 60 * 60) { + MsgCollectAllUsers(); + lastCalendarCollection = now; + } if (Collector.agent.state >= BONGO_AGENT_STATE_STOPPING) { break; } - /* Refresh every 6 hours for now */ - sleep(60*60*6); + sleep(60); } + CollectorAccountsClose(accounts); + #if VERBOSE /* Shutting down */ XplConsolePrintf(AGENT_NAME ": Shutting down.\r\n"); @@ -82,6 +103,10 @@ XplServiceMain(int argc, char *argv[]) int ccode; int startupOpts; + (void) argc; + (void) argv; + (void) GlobalConfig; + if (XplSetRealUser(MsgGetUnprivilegedUser()) < 0) { XplConsolePrintf(AGENT_NAME ": Could not drop to unprivileged user '%s'\r\n" AGENT_NAME ": exiting.\n", MsgGetUnprivilegedUser()); return -1; diff --git a/src/agents/collector/external-accounts.sql b/src/agents/collector/external-accounts.sql new file mode 100644 index 0000000..c086a50 --- /dev/null +++ b/src/agents/collector/external-accounts.sql @@ -0,0 +1,70 @@ +PRAGMA foreign_keys = ON; + +CREATE TABLE IF NOT EXISTS external_accounts ( + id INTEGER PRIMARY KEY, + owner TEXT NOT NULL, + label TEXT NOT NULL, + email_address TEXT NOT NULL, + enabled INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)), + verified_at INTEGER, + inbound_protocol TEXT NOT NULL DEFAULT 'none' + CHECK (inbound_protocol IN ('none', 'imap', 'pop3')), + inbound_host TEXT, + inbound_port INTEGER CHECK (inbound_port BETWEEN 1 AND 65535), + inbound_tls TEXT NOT NULL DEFAULT 'implicit' + CHECK (inbound_tls IN ('implicit', 'starttls')), + inbound_username TEXT, + inbound_secret_id TEXT, + poll_interval INTEGER NOT NULL DEFAULT 300 CHECK (poll_interval >= 60), + destination_folder TEXT NOT NULL DEFAULT 'INBOX', + delete_policy TEXT NOT NULL DEFAULT 'never' + CHECK (delete_policy IN ('never', 'after_import', 'after_days')), + delete_after_days INTEGER CHECK (delete_after_days > 0), + outbound_mode TEXT NOT NULL DEFAULT 'smtp' + CHECK (outbound_mode IN ('direct', 'smtp', 'proton_bridge')), + outbound_host TEXT, + outbound_port INTEGER CHECK (outbound_port BETWEEN 1 AND 65535), + outbound_tls TEXT NOT NULL DEFAULT 'starttls' + CHECK (outbound_tls IN ('implicit', 'starttls')), + outbound_username TEXT, + outbound_secret_id TEXT, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + last_checked_at INTEGER, + last_success_at INTEGER, + last_error TEXT, + UNIQUE (owner, email_address), + CHECK (delete_policy != 'after_days' OR delete_after_days IS NOT NULL), + CHECK (inbound_protocol = 'none' OR + (inbound_host IS NOT NULL AND inbound_port IS NOT NULL AND + inbound_username IS NOT NULL AND inbound_secret_id IS NOT NULL)), + CHECK (outbound_mode = 'direct' OR + (outbound_host IS NOT NULL AND outbound_port IS NOT NULL AND + outbound_username IS NOT NULL AND outbound_secret_id IS NOT NULL)) +); + +CREATE INDEX IF NOT EXISTS external_accounts_due + ON external_accounts (enabled, last_checked_at, poll_interval); + +CREATE TABLE IF NOT EXISTS external_account_messages ( + account_id INTEGER NOT NULL REFERENCES external_accounts(id) ON DELETE CASCADE, + remote_mailbox TEXT NOT NULL, + remote_uidvalidity TEXT NOT NULL DEFAULT '', + remote_uid TEXT NOT NULL, + message_id TEXT, + imported_at INTEGER NOT NULL, + delete_after INTEGER, + deleted_remote_at INTEGER, + PRIMARY KEY (account_id, remote_mailbox, remote_uidvalidity, remote_uid) +); + +CREATE INDEX IF NOT EXISTS external_account_deletions + ON external_account_messages (account_id, delete_after, deleted_remote_at); + +CREATE TABLE IF NOT EXISTS external_account_identities ( + account_id INTEGER NOT NULL REFERENCES external_accounts(id) ON DELETE CASCADE, + email_address TEXT NOT NULL, + verified_at INTEGER, + is_default INTEGER NOT NULL DEFAULT 0 CHECK (is_default IN (0, 1)), + PRIMARY KEY (account_id, email_address) +); diff --git a/src/agents/collector/external_accounts.c b/src/agents/collector/external_accounts.c new file mode 100644 index 0000000..98cc10b --- /dev/null +++ b/src/agents/collector/external_accounts.c @@ -0,0 +1,76 @@ +#include + +#include +#include +#include + +#define LOGGERNAME "collector-accounts" +#include +#include + +#include "external_accounts.h" + +extern unsigned char sql_external_accounts[]; + +sqlite3 * +CollectorAccountsOpen(void) +{ + char path[XPL_MAX_PATH]; + char *error = NULL; + sqlite3 *database = NULL; + int result; + + if (snprintf(path, sizeof(path), "%s/%s", XPL_DEFAULT_DBF_DIR, + "external-accounts.sqlite") >= (int) sizeof(path)) return NULL; + result = sqlite3_open_v2(path, &database, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE | + SQLITE_OPEN_FULLMUTEX, NULL); + if (result != SQLITE_OK) { + Log(LOG_ERROR, "Cannot open external account database %s: %s", path, + database != NULL ? sqlite3_errmsg(database) : "unknown error"); + if (database != NULL) sqlite3_close(database); + return NULL; + } + sqlite3_busy_timeout(database, 5000); + if (sqlite3_exec(database, "PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;", + NULL, NULL, &error) != SQLITE_OK || + sqlite3_exec(database, (const char *) sql_external_accounts, + NULL, NULL, &error) != SQLITE_OK) { + Log(LOG_ERROR, "Cannot initialize external account database: %s", + error != NULL ? error : sqlite3_errmsg(database)); + sqlite3_free(error); + sqlite3_close(database); + return NULL; + } + if (chmod(path, S_IRUSR | S_IWUSR) != 0) { + Log(LOG_ERROR, "Cannot protect external account database %s: %s", path, + strerror(errno)); + sqlite3_close(database); + return NULL; + } + return database; +} + +void +CollectorAccountsClose(sqlite3 *database) +{ + if (database != NULL) sqlite3_close(database); +} + +int +CollectorAccountsDueCount(sqlite3 *database, long now) +{ + sqlite3_stmt *statement = NULL; + int count = -1; + const char query[] = + "SELECT count(*) FROM external_accounts WHERE enabled = 1 AND " + "inbound_protocol != 'none' AND " + "(last_checked_at IS NULL OR last_checked_at + poll_interval <= ?)"; + + if (database == NULL || + sqlite3_prepare_v2(database, query, -1, &statement, NULL) != SQLITE_OK) return -1; + sqlite3_bind_int64(statement, 1, (sqlite3_int64) now); + if (sqlite3_step(statement) == SQLITE_ROW) count = sqlite3_column_int(statement, 0); + sqlite3_finalize(statement); + return count; +} diff --git a/src/agents/collector/external_accounts.h b/src/agents/collector/external_accounts.h new file mode 100644 index 0000000..5f4d9cd --- /dev/null +++ b/src/agents/collector/external_accounts.h @@ -0,0 +1,10 @@ +#ifndef BONGO_EXTERNAL_ACCOUNTS_H +#define BONGO_EXTERNAL_ACCOUNTS_H + +#include + +sqlite3 *CollectorAccountsOpen(void); +void CollectorAccountsClose(sqlite3 *database); +int CollectorAccountsDueCount(sqlite3 *database, long now); + +#endif diff --git a/src/agents/collector/provider-presets.json b/src/agents/collector/provider-presets.json new file mode 100644 index 0000000..5bfad08 --- /dev/null +++ b/src/agents/collector/provider-presets.json @@ -0,0 +1,144 @@ +{ + "schema_version": 1, + "reviewed": "2026-07-16", + "providers": [ + { + "id": "gmx", + "name": "GMX", + "domains": ["gmx.at", "gmx.de", "gmx.net", "gmx.com"], + "imap": {"host": "imap.gmx.net", "port": 993, "tls": "implicit"}, + "pop3": {"host": "pop.gmx.net", "port": 995, "tls": "implicit"}, + "smtp": {"host": "mail.gmx.net", "port": 587, "tls": "starttls", "alternative_port": 465}, + "authentication": ["password"], + "username": "email_address", + "setup_note": "enable_pop_imap", + "source": "https://hilfe.gmx.net/pop-imap/imap/imap-serverdaten.html" + }, + { + "id": "webde", + "name": "WEB.DE", + "domains": ["web.de"], + "imap": {"host": "imap.web.de", "port": 993, "tls": "implicit"}, + "pop3": {"host": "pop3.web.de", "port": 995, "tls": "implicit"}, + "smtp": {"host": "smtp.web.de", "port": 587, "tls": "starttls", "alternative_port": 465}, + "authentication": ["password"], + "username": "email_address", + "setup_note": "enable_pop_imap", + "source": "https://hilfe.web.de/pop-imap/imap/imap-serverdaten.html" + }, + { + "id": "gmail", + "name": "Gmail", + "domains": ["gmail.com", "googlemail.com"], + "imap": {"host": "imap.gmail.com", "port": 993, "tls": "implicit"}, + "pop3": {"host": "pop.gmail.com", "port": 995, "tls": "implicit"}, + "smtp": {"host": "smtp.gmail.com", "port": 587, "tls": "starttls", "alternative_port": 465}, + "authentication": ["oauth2", "app_password"], + "preferred_authentication": "oauth2", + "username": "email_address", + "source": "https://support.google.com/mail/answer/7126229" + }, + { + "id": "outlook", + "name": "Outlook.com / Hotmail / Live", + "domains": ["outlook.com", "hotmail.com", "hotmail.de", "live.com", "live.at", "live.de", "msn.com"], + "imap": {"host": "outlook.office365.com", "port": 993, "tls": "implicit"}, + "pop3": {"host": "outlook.office365.com", "port": 995, "tls": "implicit"}, + "smtp": {"host": "smtp-mail.outlook.com", "port": 587, "tls": "starttls"}, + "authentication": ["oauth2"], + "preferred_authentication": "oauth2", + "username": "email_address", + "setup_note": "enable_pop_imap", + "source": "https://support.microsoft.com/en-US/Outlook/pop-imap-and-smtp-settings-for-outlook-com" + }, + { + "id": "yahoo", + "name": "Yahoo Mail", + "domains": ["yahoo.com", "yahoo.de", "ymail.com", "rocketmail.com"], + "imap": {"host": "imap.mail.yahoo.com", "port": 993, "tls": "implicit"}, + "smtp": {"host": "smtp.mail.yahoo.com", "port": 465, "tls": "implicit", "alternative_port": 587}, + "authentication": ["app_password"], + "username": "email_address", + "source": "https://help.yahoo.com/kb/imap-internet-message-access-protocol-sln4075.html" + }, + { + "id": "icloud", + "name": "iCloud Mail", + "domains": ["icloud.com", "me.com", "mac.com"], + "imap": {"host": "imap.mail.me.com", "port": 993, "tls": "implicit"}, + "smtp": {"host": "smtp.mail.me.com", "port": 587, "tls": "starttls"}, + "authentication": ["app_password"], + "username": "provider_specific", + "source": "https://support.apple.com/en-ie/102525" + }, + { + "id": "aol", + "name": "AOL Mail", + "domains": ["aol.com"], + "imap": {"host": "imap.aol.com", "port": 993, "tls": "implicit"}, + "pop3": {"host": "pop.aol.com", "port": 995, "tls": "implicit"}, + "smtp": {"host": "smtp.aol.com", "port": 465, "tls": "implicit"}, + "authentication": ["app_password"], + "username": "email_address", + "source": "https://help.aol.com/articles/how-do-i-use-other-email-applications-to-send-and-receive-my-aol-mail" + }, + { + "id": "zoho", + "name": "Zoho Mail", + "domains": ["zoho.com", "zohomail.com"], + "imap": {"host": "imap.zoho.com", "port": 993, "tls": "implicit"}, + "smtp": {"host": "smtp.zoho.com", "port": 587, "tls": "starttls", "alternative_port": 465}, + "authentication": ["password", "app_password"], + "username": "email_address", + "setup_note": "availability_depends_on_region_and_plan", + "source": "https://www.zoho.com/mail/help/imap-access.html" + }, + { + "id": "freenet", + "name": "freenet Mail", + "domains": ["freenet.de"], + "imap": {"host": "mx.freenet.de", "port": 993, "tls": "implicit"}, + "pop3": {"host": "mx.freenet.de", "port": 995, "tls": "implicit"}, + "smtp": {"host": "mx.freenet.de", "port": 587, "tls": "starttls", "alternative_port": 465}, + "authentication": ["password"], + "username": "email_address", + "setup_note": "enable_pop_imap", + "source": "https://kundenservice.freenet.de/faq/e-mail-client-mail/einstellungen-serverdaten-fuer-alle-e-mail-programme/" + }, + { + "id": "mailcom", + "name": "mail.com", + "domains": ["mail.com", "email.com"], + "imap": {"host": "imap.mail.com", "port": 993, "tls": "implicit"}, + "smtp": {"host": "smtp.mail.com", "port": 587, "tls": "starttls", "alternative_port": 465}, + "authentication": ["password"], + "username": "email_address", + "setup_note": "imap_may_require_premium", + "source": "https://support.mail.com/premium/imap/server.html" + }, + { + "id": "proton", + "name": "Proton Mail", + "domains": ["proton.me", "protonmail.com", "protonmail.ch", "pm.me"], + "special_mode": "proton_bridge_or_smtp_token", + "authentication": ["bridge_password", "smtp_token"], + "setup_note": "paid_plan_or_custom_domain_restrictions", + "source": "https://proton.me/support/imap-smtp-and-pop3-setup" + }, + { + "id": "manual", + "name": "Other provider", + "domains": [], + "special_mode": "manual", + "authentication": ["password", "app_password", "oauth2"] + } + ], + "unsupported": [ + { + "id": "tuta", + "name": "Tuta Mail", + "reason": "no_imap_pop_smtp_access", + "source": "https://tuta.com/blog/desktop-clients-tutanota" + } + ] +} diff --git a/src/agents/imap/CMakeLists.txt b/src/agents/imap/CMakeLists.txt index d2bb5e2..2be35b0 100644 --- a/src/agents/imap/CMakeLists.txt +++ b/src/agents/imap/CMakeLists.txt @@ -18,6 +18,7 @@ target_link_libraries(bongoimap bongojson bongomsgapi bongostreamio + bongosasl ) install(TARGETS bongoimap DESTINATION ${SBIN_INSTALL_DIR}) diff --git a/src/agents/imap/copy.c b/src/agents/imap/copy.c index c0c207e..08ba8e7 100644 --- a/src/agents/imap/copy.c +++ b/src/agents/imap/copy.c @@ -34,7 +34,7 @@ CopyMessageRangeToTarget(ImapSession *session, MessageInformation *message, count = rangeCount; do { - if (NMAPSendCommandF(storeConn, "COPY %llx %llx\r\n", message->guid, target) != -1) { + if (NMAPSendCommandF(storeConn, "COPY %" PRIx64 " %" PRIx64 "\r\n", message->guid, target) != -1) { if ((ccode = NMAPReadResponse(storeConn, NULL, 0, 0)) == 1000) { count--; if (count > 0) { @@ -86,8 +86,8 @@ __inline static long HandleCopy(ImapSession *session, BOOL ByUID) { FolderInformation *targetFolder; - unsigned char *messageSet; - unsigned char *ptr; + char *messageSet; + char *ptr; FolderPath targetPath; long ccode; char *ptr2; diff --git a/src/agents/imap/event.c b/src/agents/imap/event.c index 06a0b8a..85ebf2b 100644 --- a/src/agents/imap/event.c +++ b/src/agents/imap/event.c @@ -291,7 +291,7 @@ MessageListAddNewMessages(Connection *storeConn, OpenedFolder *folder, NewEvent long ccode; long result; long headerSize; - unsigned char reply[1024]; + char reply[1024]; do { result = STATUS_CONTINUE; @@ -377,6 +377,8 @@ MarkPurgedMessages(Connection *clientConn, StoreEvents *events, OpenedFolder *fo unsigned long messageId; unsigned long count; + UNUSED_PARAMETER(clientConn); + purge = &(events->purge[0]); count = events->purgeCount; @@ -408,12 +410,18 @@ SendRememberedEvents(Connection *clientConn, Connection *storeConn, OpenedFolder if (typesAllowed & STORE_EVENT_PURGE) { sequenceChanged = TRUE; ccode = SendPurgeEvents(clientConn, events, selectedFolder); + if (ccode != STATUS_CONTINUE) { + return ccode; + } events->purgeCount = 0; events->remembered &= ~STORE_EVENT_PURGE; } else { /* we can't send anything to the client, but we do need to update */ /* the messageList so commands do not try to access these messages */ ccode = MarkPurgedMessages(clientConn, events, selectedFolder); + if (ccode != STATUS_CONTINUE) { + return ccode; + } } } @@ -449,7 +457,7 @@ GenerateEvents(OpenedFolder *selectedFolder, OpenedFolder *updatedFolder) if (oldListMessagesLeft > 0) { if (newListMessage->uid == oldListMessage->uid) { /* Message in both lists */ - if (newListMessage->flags != newListMessage->flags) { + if (newListMessage->flags != oldListMessage->flags) { RememberFlagEvent(&(selectedFolder->events), newListMessage->uid, newListMessage->flags); } oldListMessage++; diff --git a/src/agents/imap/fetch.c b/src/agents/imap/fetch.c index f11f437..541444d 100644 --- a/src/agents/imap/fetch.c +++ b/src/agents/imap/fetch.c @@ -1,20 +1,20 @@ /**************************************************************************** * * Copyright (c) 2001 Novell, Inc. All Rights Reserved. - * + * * This program is free software; you can redistribute it and/or * modify it under the terms of version 2 of the GNU General Public License * as published by the Free Software Foundation. - * + * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. - * + * * You should have received a copy of the GNU General Public License * along with this program; if not, contact Novell, Inc. - * - * To contact Novell about this file by physical or electronic mail, you + * + * To contact Novell about this file by physical or electronic mail, you * may find current contact information at www.novell.com. * ****************************************************************************/ @@ -34,13 +34,13 @@ BongoKeywordIndex *FetchFlagsAttIndex = NULL; BongoKeywordIndex *FetchFlagsSoloIndex = NULL; BongoKeywordIndex *FetchFlagsSectionIndex = NULL; -unsigned char TopLevelMime[] = "Content-Type: message/rfc822\r\nContent-Tranfer-Encoding: 7bit\r\n"; +char TopLevelMime[] = "Content-Type: message/rfc822\r\nContent-Tranfer-Encoding: 7bit\r\n"; unsigned long TopLevelMimeLen = sizeof(TopLevelMime) - 1; static BOOL -FindRFC822Header(unsigned char *Header, unsigned char *SearchFor, unsigned char **Buffer, int BufSize) +FindRFC822Header(char *Header, char *SearchFor, char **Buffer, int BufSize) { - unsigned char *NL, *ptr, *ptr2, *start=Header, *ptr3; + char *NL, *ptr, *ptr2, *ptr3; BOOL KeepGoing=TRUE; int len; @@ -63,7 +63,7 @@ FindRFC822Header(unsigned char *Header, unsigned char *SearchFor, unsigned char ptr2=strchr(ptr, ':'); if (ptr2) { *ptr2='\0'; - + if (XplStrCaseCmp(ptr, SearchFor)==0) { ptr3=ptr2+1; while (IsWhiteSpace(*ptr3)) @@ -100,30 +100,31 @@ FindRFC822Header(unsigned char *Header, unsigned char *SearchFor, unsigned char KeepGoing=FALSE; } - start=ptr; } return(FALSE); } static long -SendRFC822Address(ImapSession *session, unsigned char *Address, unsigned char *Answer) +SendRFC822Address(ImapSession *session, char *Address, char *Answer) { long ccode = 0; RFC822AddressStruct *Current; RFC822AddressStruct *Rfc822AddressList = NULL; unsigned long Len; + UNUSED_PARAMETER(Answer); + RFC822ParseAddressList(&Rfc822AddressList, Address, ".MissingHostName."); if (Rfc822AddressList) { if (ConnWrite(session->client.conn, "(", 1) != -1) { - Current = Rfc822AddressList; + Current = Rfc822AddressList; while (Current) { if (Current->Personal) { Len = strlen(Current->Personal); - if ((ccode = ConnWriteF(session->client.conn, "({%lu}\r\n", Len) != -1) && - (ccode = ConnWrite(session->client.conn, Current->Personal, Len) != -1) && + if ((ccode = ConnWriteF(session->client.conn, "({%lu}\r\n", Len) != -1) && + (ccode = ConnWrite(session->client.conn, Current->Personal, Len) != -1) && (ccode = ConnWrite(session->client.conn, " ", 1)) != -1) { ; } else { @@ -138,8 +139,8 @@ SendRFC822Address(ImapSession *session, unsigned char *Address, unsigned char *A } if (Current->ADL) { - if (((ccode = ConnWrite(session->client.conn, "\"", 1)) != -1) && - ((ccode = ConnWrite(session->client.conn, Current->ADL, strlen(Current->ADL))) != -1) && + if (((ccode = ConnWrite(session->client.conn, "\"", 1)) != -1) && + ((ccode = ConnWrite(session->client.conn, Current->ADL, strlen(Current->ADL))) != -1) && ((ccode = ConnWrite(session->client.conn, "\" ", 2)) != -1)) { ; } else { @@ -154,8 +155,8 @@ SendRFC822Address(ImapSession *session, unsigned char *Address, unsigned char *A } if (Current->Mailbox) { - if (((ccode = ConnWrite(session->client.conn, "\"", 1)) != -1) && - ((ccode = ConnWrite(session->client.conn, Current->Mailbox, strlen(Current->Mailbox))) != -1) && + if (((ccode = ConnWrite(session->client.conn, "\"", 1)) != -1) && + ((ccode = ConnWrite(session->client.conn, Current->Mailbox, strlen(Current->Mailbox))) != -1) && ((ccode = ConnWrite(session->client.conn, "\" ", 2)) != -1)) { ; } else { @@ -170,8 +171,8 @@ SendRFC822Address(ImapSession *session, unsigned char *Address, unsigned char *A } if (Current->Host) { - if (((ccode = ConnWrite(session->client.conn, "\"", 1)) != -1) && - ((ccode = ConnWrite(session->client.conn, Current->Host, strlen(Current->Host))) != -1) && + if (((ccode = ConnWrite(session->client.conn, "\"", 1)) != -1) && + ((ccode = ConnWrite(session->client.conn, Current->Host, strlen(Current->Host))) != -1) && ((ccode = ConnWrite(session->client.conn, "\")", 2)) != -1)) { ; } else { @@ -192,7 +193,7 @@ SendRFC822Address(ImapSession *session, unsigned char *Address, unsigned char *A if (ConnWrite(session->client.conn, ") ", 2) != -1) { return(STATUS_CONTINUE); } - + } return(STATUS_ABORT); } @@ -208,9 +209,9 @@ SendRFC822Address(ImapSession *session, unsigned char *Address, unsigned char *A } static void -MakeRFC822Header(unsigned char *Header, unsigned long *HSize) +MakeRFC822Header(char *Header, unsigned long *HSize) { - unsigned char *NL, *ptr, *ptr2, *start=Header; + char *NL, *ptr, *ptr2, *start=Header; BOOL StripLine, KeepGoing=TRUE; ptr=Header; @@ -256,18 +257,18 @@ MakeRFC822Header(unsigned char *Header, unsigned long *HSize) *HSize=strlen(Header); } -__inline static unsigned char * -GetSubHeader(unsigned char *header, unsigned long headerSize, unsigned char **fields, unsigned long fieldCount, BOOL not, unsigned long *newHeaderLen) +__inline static char * +GetSubHeader(char *header, unsigned long headerSize, char **fields, unsigned long fieldCount, BOOL not, unsigned long *newHeaderLen) { - unsigned char *newHeader; - unsigned char *start; - unsigned char *colon; - unsigned char *newLine; - unsigned char *dest; + char *newHeader; + char *start; + char *colon; + char *newLine; + char *dest; unsigned long i; unsigned long lineLen; BOOL keepLine; - + newHeader = MemMalloc(headerSize); if (newHeader) { start = header; @@ -289,7 +290,7 @@ GetSubHeader(unsigned char *header, unsigned long headerSize, unsigned char **fi keepLine = not; break; } - + /* this line is a match */ keepLine = !not; break; @@ -341,9 +342,9 @@ GetSubHeader(unsigned char *header, unsigned long headerSize, unsigned char **fi } __inline static void -ParseMIMEDLine(unsigned char *MIME, unsigned char *Type, unsigned long TypeSize, unsigned char *SubType, unsigned long SubTypeSize, unsigned char *Charset, unsigned long CharsetSize, unsigned char *Encoding, unsigned long EncodingSize, unsigned char *Name, unsigned long NameSize, long *headStart, unsigned long *headSize, unsigned long *StartPos, unsigned long *Size, unsigned long *HeaderSize, unsigned long *Lines) +ParseMIMEDLine(char *MIME, char *Type, unsigned long TypeSize, char *SubType, unsigned long SubTypeSize, char *Charset, unsigned long CharsetSize, char *Encoding, unsigned long EncodingSize, char *Name, unsigned long NameSize, long *headStart, unsigned long *headSize, unsigned long *StartPos, unsigned long *Size, unsigned long *HeaderSize, unsigned long *Lines) { - unsigned char *p, *p2; + char *p, *p2; /* Type */ p2 = strchr(MIME, ' '); if (p2) { @@ -483,7 +484,7 @@ FreeFetchResourcesSuccess(FetchStruct *FetchRequest) if (!(FetchRequest->hasAllocated & ALLOCATED_FLAGS)) { return; } - + MemFree(FetchRequest->flag); return; } @@ -497,7 +498,7 @@ FreeFetchResourcesFailure(FetchStruct *FetchRequest) } __inline static long -ReadBodyPart(ImapSession *session, unsigned char **Part, unsigned long ID, unsigned long Start, unsigned long Size) +ReadBodyPart(ImapSession *session, char **Part, unsigned long ID, unsigned long Start, unsigned long Size) { size_t count; long ccode; @@ -506,19 +507,19 @@ ReadBodyPart(ImapSession *session, unsigned char **Part, unsigned long ID, unsig if (*Part) { if (Size) { /* Request the body part*/ - if (NMAPSendCommandF(session->store.conn, "READ %llx %lu %lu\r\n", session->folder.selected.message[ID].guid, Start, Size) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu %lu\r\n", session->folder.selected.message[ID].guid, Start, Size) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { if (NMAPReadCount(session->store.conn, *Part, count) == (int)count) { if (NMAPReadCrLf(session->store.conn) == 2) { return(STATUS_CONTINUE); } } - + MemFree(*Part); *Part = NULL; return(STATUS_ABORT); } - + MemFree(*Part); *Part = NULL; return(CheckForNMAPCommError(ccode)); @@ -537,9 +538,9 @@ ReadBodyPart(ImapSession *session, unsigned char **Part, unsigned long ID, unsig } __inline static long -SendEnvelope(ImapSession *session, unsigned char *Header, unsigned char *Answer) +SendEnvelope(ImapSession *session, char *Header, char *Answer) { - unsigned char *component = NULL; + char *component = NULL; long ccode; unsigned long len; @@ -569,8 +570,8 @@ SendEnvelope(ImapSession *session, unsigned char *Header, unsigned char *Answer) /* now env_subject */ if (FindRFC822Header(Header, "Subject", &component, 0)) { len=strlen(component); - if ((ConnWriteF(session->client.conn, "{%d}\r\n", (int)len) != -1) && - (ConnWrite(session->client.conn, component, len) != -1) && + if ((ConnWriteF(session->client.conn, "{%d}\r\n", (int)len) != -1) && + (ConnWrite(session->client.conn, component, len) != -1) && (ConnWrite(session->client.conn, " ", 1) != -1)) { MemFree(component); component = NULL; @@ -578,7 +579,7 @@ SendEnvelope(ImapSession *session, unsigned char *Header, unsigned char *Answer) MemFree(component); return(STATUS_ABORT); } - + } else { if (ConnWrite(session->client.conn, "NIL ", 4) != -1) { ; @@ -711,16 +712,17 @@ SendEnvelope(ImapSession *session, unsigned char *Header, unsigned char *Answer) /* now env_in_reply_to */ if (FindRFC822Header(Header, "In-Reply-To", &component, 0)) { - unsigned char *ptr = component; + char *ptr = component; int inRepToLen = strlen(component); /* Break up reply to session, Answer may not be large enough */ /* to hold all of the recipients in the reply to field. */ do { - if (*ptr == '"' || *ptr == '\\' || *ptr < 0x20 || *ptr > 0x7f) { - if ((ConnWriteF(session->client.conn, "{%lu}\r\n", (long unsigned int)inRepToLen) != -1) && - (ConnWrite(session->client.conn, component, inRepToLen) != -1) && + if (*ptr == '"' || *ptr == '\\' || (unsigned char)*ptr < 0x20 || + (unsigned char)*ptr > 0x7f) { + if ((ConnWriteF(session->client.conn, "{%lu}\r\n", (long unsigned int)inRepToLen) != -1) && + (ConnWrite(session->client.conn, component, inRepToLen) != -1) && (ConnWrite(session->client.conn, " ", 1) != -1)) { break; } else { @@ -728,13 +730,13 @@ SendEnvelope(ImapSession *session, unsigned char *Header, unsigned char *Answer) return(STATUS_ABORT); } } - + if (*(++ptr)) { continue; } - if ((ConnWrite(session->client.conn, "\"", 1) != -1) && - (ConnWrite(session->client.conn, component, inRepToLen) != -1) && + if ((ConnWrite(session->client.conn, "\"", 1) != -1) && + (ConnWrite(session->client.conn, component, inRepToLen) != -1) && (ConnWrite(session->client.conn, "\" ", 2) != -1)) { break; } else { @@ -754,8 +756,8 @@ SendEnvelope(ImapSession *session, unsigned char *Header, unsigned char *Answer) /* now env_message_id */ if (FindRFC822Header(Header, "Message-Id", &component, 0)) { - if ((ConnWrite(session->client.conn, "\"", 1) != -1) && - (ConnWrite(session->client.conn, component, strlen(component)) != -1) && + if ((ConnWrite(session->client.conn, "\"", 1) != -1) && + (ConnWrite(session->client.conn, component, strlen(component)) != -1) && (ConnWrite(session->client.conn, "\"", 1) != -1)) { MemFree(component); component = NULL; @@ -779,9 +781,10 @@ SendEnvelope(ImapSession *session, unsigned char *Header, unsigned char *Answer) return(STATUS_ABORT); } -static long +static long FetchFlagResponderUid(void *param1, void *param2, void *param3) { + UNUSED_PARAMETER(param3); ImapSession *session = (ImapSession *)param1; FetchStruct *FetchRequest = (FetchStruct *)param2; @@ -791,12 +794,13 @@ FetchFlagResponderUid(void *param1, void *param2, void *param3) return(STATUS_ABORT); } -static long +static long FetchFlagResponderFlags(void *param1, void *param2, void *param3) { + UNUSED_PARAMETER(param3); ImapSession *session = (ImapSession *)param1; FetchStruct *FetchRequest = (FetchStruct *)param2; - unsigned char addSpace[2]; + char addSpace[2]; long ccode; ccode = ConnWrite(session->client.conn, "FLAGS (", strlen("FLAGS (")); @@ -806,15 +810,15 @@ FetchFlagResponderFlags(void *param1, void *param2, void *param3) if ((FetchRequest->message->flags & STORE_MSG_FLAG_ANSWERED) && (ccode != -1)) { ccode = ConnWriteF(session->client.conn, "\\Answered"); addSpace[0]=' '; - } + } if ((FetchRequest->message->flags & STORE_MSG_FLAG_DELETED) && (ccode != -1)) { ccode = ConnWriteF(session->client.conn, "%s\\Deleted",addSpace); addSpace[0]=' '; - } + } if ((FetchRequest->message->flags & STORE_MSG_FLAG_RECENT) && (ccode != -1)) { ccode = ConnWriteF(session->client.conn, "%s\\Recent", addSpace); addSpace[0]=' '; - } + } if ((FetchRequest->message->flags & STORE_MSG_FLAG_SEEN) && (ccode != -1)) { ccode = ConnWriteF(session->client.conn, "%s\\Seen", addSpace); addSpace[0]=' '; @@ -827,20 +831,21 @@ FetchFlagResponderFlags(void *param1, void *param2, void *param3) ccode = ConnWriteF(session->client.conn, "%s\\Flagged", addSpace); addSpace[0]=' '; } - + if (ccode != -1) { ccode = ConnWrite(session->client.conn, ")", strlen(")")); if (ccode != -1) { return(STATUS_CONTINUE); } } - + return(STATUS_ABORT); } -static long +static long FetchFlagResponderEnvelope(void *param1, void *param2, void *param3) { + UNUSED_PARAMETER(param3); ImapSession *session = (ImapSession *)param1; FetchStruct *FetchRequest = (FetchStruct *)param2; if (ConnWrite(session->client.conn, "ENVELOPE ", strlen("ENVELOPE ")) != -1) { @@ -849,9 +854,10 @@ FetchFlagResponderEnvelope(void *param1, void *param2, void *param3) return(STATUS_ABORT); } -static long +static long FetchFlagResponderXSender(void *param1, void *param2, void *param3) { + UNUSED_PARAMETER(param3); ImapSession *session = (ImapSession *)param1; FetchStruct *FetchRequest = (FetchStruct *)param2; long ccode; @@ -871,16 +877,16 @@ FetchFlagResponderXSender(void *param1, void *param2, void *param3) authLen = strlen(session->store.response); if (!strchr(session->store.response, '@')) { size_t hostlen = strlen(BongoGlobals.hostname); - if ((ConnWriteF(session->client.conn, "XSENDER {%lu}\r\n", authLen + 1 + hostlen) != -1) && - (ConnWrite(session->client.conn, session->store.response, authLen) != -1) && - (ConnWrite(session->client.conn, "@", 1) != -1) && + if ((ConnWriteF(session->client.conn, "XSENDER {%lu}\r\n", authLen + 1 + hostlen) != -1) && + (ConnWrite(session->client.conn, session->store.response, authLen) != -1) && + (ConnWrite(session->client.conn, "@", 1) != -1) && (ConnWrite(session->client.conn, BongoGlobals.hostname, hostlen) != -1)) { return(STATUS_CONTINUE); - } + } return(STATUS_ABORT); } - if ((ConnWriteF(session->client.conn, "XSENDER {%lu}\r\n", authLen) != -1) && + if ((ConnWriteF(session->client.conn, "XSENDER {%lu}\r\n", authLen) != -1) && (ConnWrite(session->client.conn, session->store.response, authLen) != -1)) { return(STATUS_CONTINUE); } @@ -896,8 +902,12 @@ FetchFlagResponderInternalDate(void *param1, void *param2, void *param3) ImapSession *session = (ImapSession *)param1; FetchStruct *FetchRequest = (FetchStruct *)param2; struct tm ltm; + time_t internalDate; - gmtime_r(&FetchRequest->message->internalDate, <m); + UNUSED_PARAMETER(param3); + + internalDate = (time_t)FetchRequest->message->internalDate; + gmtime_r(&internalDate, <m); if (ConnWriteF(session->client.conn, "INTERNALDATE \"%02d-%3s-%04d %02d:%02d:%02d +0000\"", ltm.tm_mday, Imap.command.months[ltm.tm_mon], 1900+ltm.tm_year, ltm.tm_hour, ltm.tm_min, ltm.tm_sec) != -1) { return(STATUS_CONTINUE); @@ -905,23 +915,23 @@ FetchFlagResponderInternalDate(void *param1, void *param2, void *param3) return(STATUS_ABORT); } -static long +static long FetchFlagResponderBodyStructure(void *param1, void *param2, void *param3) { ImapSession *session = (ImapSession *)param1; FetchStruct *FetchRequest = (FetchStruct *)param2; - unsigned char **multiPartTypeList; + char **multiPartTypeList; long *rfcSizeList; long ccode; - unsigned char Type[MIME_TYPE_LEN+1]; - unsigned char Subtype[MIME_SUBTYPE_LEN+1]; - unsigned char Charset[MIME_CHARSET_LEN+1]; - unsigned char Encoding[MIME_ENCODING_LEN+1]; - unsigned char Name[MIME_NAME_LEN+1]; - unsigned long mimeOffset; + char Type[MIME_TYPE_LEN+1]; + char Subtype[MIME_SUBTYPE_LEN+1]; + char Charset[MIME_CHARSET_LEN+1]; + char Encoding[MIME_ENCODING_LEN+1]; + char Name[MIME_NAME_LEN+1]; + long mimeOffset; unsigned long mimeLen; - unsigned long SPos; - unsigned long Lines; + unsigned long SPos; + unsigned long Lines; unsigned long Size; unsigned long MPCount; unsigned long RFCCount; @@ -930,7 +940,9 @@ FetchFlagResponderBodyStructure(void *param1, void *param2, void *param3) unsigned long RFCSizeListLen = 20; unsigned long mimeResponseLine = 0; - multiPartTypeList = MemMalloc(sizeof(unsigned char *)*MultiPartTypeListLen); + UNUSED_PARAMETER(param3); + + multiPartTypeList = MemMalloc(sizeof(char *)*MultiPartTypeListLen); if (multiPartTypeList) { MPCount = 0; @@ -950,18 +962,18 @@ FetchFlagResponderBodyStructure(void *param1, void *param2, void *param3) char *, mimeResponseLine); switch(atol(mime_string)) { case 2002: { - ParseMIMEDLine(mime_string + 5, - Type, MIME_TYPE_LEN, - Subtype, MIME_SUBTYPE_LEN, - Charset, MIME_CHARSET_LEN, - Encoding, MIME_ENCODING_LEN, - Name, MIME_NAME_LEN, + ParseMIMEDLine(mime_string + 5, + Type, MIME_TYPE_LEN, + Subtype, MIME_SUBTYPE_LEN, + Charset, MIME_CHARSET_LEN, + Encoding, MIME_ENCODING_LEN, + Name, MIME_NAME_LEN, &mimeOffset, &mimeLen, - &SPos, - &Size, - &partHeaderSize, - &Lines); + &SPos, + &Size, + &partHeaderSize, + &Lines); /* First, set the default values if nothing is specified by the message */ if (Type[0]=='-') { @@ -977,25 +989,25 @@ FetchFlagResponderBodyStructure(void *param1, void *param2, void *param3) **MULTIPART** ************/ if (XplStrCaseCmp(Type, "Multipart")==0) { - if (MPCount < MultiPartTypeListLen) { + if (MPCount < MultiPartTypeListLen) { multiPartTypeList[MPCount++] = MemStrdup(Subtype); } else { - unsigned char *tmp; + char *tmp; MultiPartTypeListLen <<= 1; /* Double the Size */ - tmp = MemRealloc(multiPartTypeList, sizeof(unsigned char *) * MultiPartTypeListLen); + tmp = MemRealloc(multiPartTypeList, sizeof(char *) * MultiPartTypeListLen); if (tmp) { - multiPartTypeList = (unsigned char **)tmp; + multiPartTypeList = (char **)tmp; multiPartTypeList[MPCount++] = MemStrdup(Subtype); } else { while (MPCount > 0) { - MPCount--; + MPCount--; MemFree(multiPartTypeList[MPCount]); } MemFree(multiPartTypeList); MemFree(rfcSizeList); return(STATUS_MEMORY_ERROR); - } + } } if (ConnWrite(session->client.conn, "(", 1) != -1) { ; @@ -1022,29 +1034,29 @@ FetchFlagResponderBodyStructure(void *param1, void *param2, void *param3) ** MESSAGE ** ************/ } else if ((XplStrCaseCmp(Type, "Message")==0) && (XplStrCaseCmp(Subtype, "RFC822")==0)) { - unsigned char *PartHeader; + char *PartHeader; /* we have to print the basic stuff, then get the headers, print the envelope */ /* and then continue in the loop */ if (RFCCount < RFCSizeListLen) { rfcSizeList[RFCCount++] = Lines; } else { - unsigned char *tmp; + char *tmp; RFCSizeListLen <<= 1; /* Double the Size */ tmp = MemRealloc(rfcSizeList, sizeof(long) * RFCSizeListLen); if (tmp) { - rfcSizeList = (long *)tmp; + rfcSizeList = (long *)tmp; rfcSizeList[RFCCount++] = Lines; } else { while (MPCount > 0) { - MPCount--; + MPCount--; MemFree(multiPartTypeList[MPCount]); } MemFree(multiPartTypeList); MemFree(rfcSizeList); return(STATUS_MEMORY_ERROR); - } + } } /* Body Type & Subtype */ @@ -1058,7 +1070,7 @@ FetchFlagResponderBodyStructure(void *param1, void *param2, void *param3) /* Body parameter parentesized list */ if ((Charset[0]!='-') && (Name[0]!='\0')) { - unsigned char *langPtr; + char *langPtr; if ((Name[0] != '*') || (Name[1] != '=') || ((langPtr = strchr(Name + 2, '\'')) == NULL) || ((langPtr = strchr(langPtr + 1, '\'')) == NULL)) { if (ConnWriteF(session->client.conn, "(\"CHARSET\" \"%s\" \"NAME\" \"%s\") ", Charset, Name) != -1) { ; @@ -1069,7 +1081,7 @@ FetchFlagResponderBodyStructure(void *param1, void *param2, void *param3) } } else { /* this is an rfc2231 name */ - + if (ConnWriteF(session->client.conn, "(\"CHARSET\" \"%s\" \"NAME*\" \"%s\") ", Charset, Name + 2) != -1) { ; } else { @@ -1088,7 +1100,7 @@ FetchFlagResponderBodyStructure(void *param1, void *param2, void *param3) } } else if (Name[0]!='\0') { - unsigned char *langPtr; + char *langPtr; if ((Name[0] != '*') || (Name[1] != '=') || ((langPtr = strchr(Name + 2, '\'')) == NULL) || ((langPtr = strchr(langPtr + 1, '\'')) == NULL)) { if (ConnWriteF(session->client.conn, "(\"NAME\" \"%s\") ", Name) != -1) { ; @@ -1137,7 +1149,7 @@ FetchFlagResponderBodyStructure(void *param1, void *param2, void *param3) } /* Grab the envelope */ - + if ((ccode = ReadBodyPart(session, &PartHeader, FetchRequest->messageDetail.sequenceNumber, SPos, partHeaderSize)) == STATUS_CONTINUE) { ccode = SendEnvelope(session, PartHeader, session->store.response); MemFree(PartHeader); @@ -1169,7 +1181,7 @@ FetchFlagResponderBodyStructure(void *param1, void *param2, void *param3) /* Body parameter parentesized list */ if ((Charset[0]!='-') && (Name[0]!='\0')) { - unsigned char *langPtr; + char *langPtr; if ((Name[0] != '*') || (Name[1] != '=') || ((langPtr = strchr(Name + 2, '\'')) == NULL) || ((langPtr = strchr(langPtr + 1, '\'')) == NULL)) { if (ConnWriteF(session->client.conn, "(\"CHARSET\" \"%s\" \"NAME\" \"%s\") ", Charset, Name) != -1) { ; @@ -1197,7 +1209,7 @@ FetchFlagResponderBodyStructure(void *param1, void *param2, void *param3) return(STATUS_ABORT); } } else if (Name[0]!='\0') { - unsigned char *langPtr; + char *langPtr; if ((Name[0] != '*') || (Name[1] != '=') || ((langPtr = strchr(Name + 2, '\'')) == NULL) || ((langPtr = strchr(langPtr + 1, '\'')) == NULL)) { if (ConnWriteF(session->client.conn, "(\"NAME\" \"%s\") ", Name) != -1) { ; @@ -1301,15 +1313,16 @@ FetchFlagResponderBodyStructure(void *param1, void *param2, void *param3) return(STATUS_MEMORY_ERROR); } -static long +static long FetchFlagResponderRfc822Text(void *param1, void *param2, void *param3) { + UNUSED_PARAMETER(param3); ImapSession *session = (ImapSession *)param1; FetchStruct *FetchRequest = (FetchStruct *)param2; long ccode; size_t count; - if (NMAPSendCommandF(session->store.conn, "READ %llx %lu\r\n", FetchRequest->message->guid, FetchRequest->message->headerSize) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu\r\n", FetchRequest->message->guid, FetchRequest->message->headerSize) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { if (ConnWriteF(session->client.conn, "RFC822.TEXT {%lu}\r\n", (long)count) != -1) { if (ConnReadToConn(session->store.conn, session->client.conn, count) == (int)count) { @@ -1327,9 +1340,10 @@ FetchFlagResponderRfc822Text(void *param1, void *param2, void *param3) return(STATUS_NMAP_COMM_ERROR); } -static long +static long FetchFlagResponderRfc822Size(void *param1, void *param2, void *param3) { + UNUSED_PARAMETER(param3); ImapSession *session = (ImapSession *)param1; FetchStruct *FetchRequest = (FetchStruct *)param2; @@ -1339,9 +1353,10 @@ FetchFlagResponderRfc822Size(void *param1, void *param2, void *param3) return(STATUS_ABORT); } -static long +static long FetchFlagResponderRfc822Header(void *param1, void *param2, void *param3) { + UNUSED_PARAMETER(param3); ImapSession *session = (ImapSession *)param1; FetchStruct *FetchRequest = (FetchStruct *)param2; @@ -1356,12 +1371,13 @@ FetchFlagResponderRfc822Header(void *param1, void *param2, void *param3) static long FetchFlagResponderRfc822(void *param1, void *param2, void *param3) { + UNUSED_PARAMETER(param3); ImapSession *session = (ImapSession *)param1; FetchStruct *FetchRequest = (FetchStruct *)param2; long ccode; size_t count; - if (NMAPSendCommandF(session->store.conn, "READ %llx %lu\r\n", FetchRequest->message->guid, FetchRequest->message->headerSize) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu\r\n", FetchRequest->message->guid, FetchRequest->message->headerSize) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { if (ConnWriteF(session->client.conn, "RFC822 {%lu}\r\n", count + FetchRequest->message->headerSize) != -1) { if (ConnWrite(session->client.conn, FetchRequest->messageDetail.header, FetchRequest->message->headerSize) != -1) { @@ -1372,7 +1388,7 @@ FetchFlagResponderRfc822(void *param1, void *param2, void *param3) } } } - + return(STATUS_ABORT); } @@ -1388,9 +1404,10 @@ FetchFlagResponderBody(void *param1, void *param2, void *param3) return(FetchFlagResponderBodyStructure(param1, param2, param3)); } -static long +static long FetchFlagResponderBodyHeader(void *param1, void *param2, void *param3) { + UNUSED_PARAMETER(param3); ImapSession *session = (ImapSession *)param1; FetchStruct *FetchRequest = (FetchStruct *)param2; @@ -1436,12 +1453,13 @@ FetchFlagResponderBodyHeaderPartial(void *param1, void *param2, void *param3) static long FetchFlagResponderBodyText(void *param1, void *param2, void *param3) { + UNUSED_PARAMETER(param3); ImapSession *session = (ImapSession *)param1; FetchStruct *FetchRequest = (FetchStruct *)param2; long ccode; size_t count; - if (NMAPSendCommandF(session->store.conn, "READ %llx %lu\r\n", FetchRequest->message->guid, FetchRequest->message->headerSize) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu\r\n", FetchRequest->message->guid, FetchRequest->message->headerSize) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { if (ConnWriteF(session->client.conn, "BODY[TEXT] {%lu}\r\n", (long)count) != -1) { if (ConnReadToConn(session->store.conn, session->client.conn, count) == (int)count) { @@ -1453,7 +1471,7 @@ FetchFlagResponderBodyText(void *param1, void *param2, void *param3) return(STATUS_ABORT); } - + return(CheckForNMAPCommError(ccode)); } @@ -1471,9 +1489,9 @@ FetchFlagResponderBodyTextPartial(void *param1, void *param2, void *param3) if (flag->partial.start < FetchRequest->message->bodySize) { if ((flag->partial.start + flag->partial.len) < FetchRequest->message->bodySize) { - ccode = NMAPSendCommandF(session->store.conn, "READ %llx %lu %lu\r\n", FetchRequest->message->guid, FetchRequest->message->headerSize + flag->partial.start, flag->partial.len); + ccode = NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu %lu\r\n", FetchRequest->message->guid, FetchRequest->message->headerSize + flag->partial.start, flag->partial.len); } else { - ccode = NMAPSendCommandF(session->store.conn, "READ %llx %lu %lu\r\n", FetchRequest->message->guid, FetchRequest->message->headerSize + flag->partial.start, FetchRequest->message->bodySize - flag->partial.start); + ccode = NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu %lu\r\n", FetchRequest->message->guid, FetchRequest->message->headerSize + flag->partial.start, FetchRequest->message->bodySize - flag->partial.start); } if (ccode != -1) { @@ -1504,12 +1522,13 @@ FetchFlagResponderBodyTextPartial(void *param1, void *param2, void *param3) static long FetchFlagResponderBodyBoth(void *param1, void *param2, void *param3) { + UNUSED_PARAMETER(param3); ImapSession *session = (ImapSession *)param1; FetchStruct *FetchRequest = (FetchStruct *)param2; long ccode; size_t count; - if (NMAPSendCommandF(session->store.conn, "READ %llx\r\n", FetchRequest->message->guid) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 "\r\n", FetchRequest->message->guid) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { if (ConnWriteF(session->client.conn, "BODY[] {%lu}\r\n", (long)count) != -1) { if (ConnReadToConn(session->store.conn, session->client.conn, count) == (int)count) { @@ -1538,7 +1557,7 @@ FetchFlagResponderBodyBothPartial(void *param1, void *param2, void *param3) size_t count; if (flag->partial.start < FetchRequest->message->size) { - if (NMAPSendCommandF(session->store.conn, "READ %llx %lu %lu\r\n", FetchRequest->message->guid, flag->partial.start, flag->partial.len) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu %lu\r\n", FetchRequest->message->guid, flag->partial.start, flag->partial.len) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { if (ConnWriteF(session->client.conn, "BODY[]<%lu> {%lu}\r\n", flag->partial.start, (long)count) != -1) { if (ConnReadToConn(session->store.conn, session->client.conn, count) == (int)count) { @@ -1563,9 +1582,11 @@ FetchFlagResponderBodyBothPartial(void *param1, void *param2, void *param3) return(STATUS_ABORT); } -static long +static long FetchFlagResponderBodyMime(void *param1, void *param2, void *param3) { + UNUSED_PARAMETER(param2); + UNUSED_PARAMETER(param3); ImapSession *session = (ImapSession *)param1; if (ConnWriteF(session->client.conn, "BODY[MIME] {%lu}\r\n", TopLevelMimeLen) != -1) { @@ -1579,6 +1600,7 @@ FetchFlagResponderBodyMime(void *param1, void *param2, void *param3) static long FetchFlagResponderBodyMimePartial(void *param1, void *param2, void *param3) { + UNUSED_PARAMETER(param2); ImapSession *session = (ImapSession *)param1; FetchFlagStruct *flag = (FetchFlagStruct *)param3; @@ -1614,7 +1636,7 @@ FetchFlagResponderBodyHeaderFields(void *param1, void *param2, void *param3) FetchFlagStruct *flag = (FetchFlagStruct *)param3; long ccode; unsigned long j; - unsigned char *resultHeader; + char *resultHeader; unsigned long resultHeaderLen; if (!flag->fieldList.skip) { @@ -1631,8 +1653,8 @@ FetchFlagResponderBodyHeaderFields(void *param1, void *param2, void *param3) } return(STATUS_ABORT); - } - + } + if (ConnWriteF(session->client.conn, "\"%s\")]", flag->fieldList.field[j]) != -1) { resultHeader = GetSubHeader(FetchRequest->messageDetail.header, FetchRequest->message->headerSize, flag->fieldList.field, flag->fieldList.count, flag->fieldList.skip, &resultHeaderLen); if (resultHeader) { @@ -1640,7 +1662,7 @@ FetchFlagResponderBodyHeaderFields(void *param1, void *param2, void *param3) if (resultHeaderLen > 0) { if (ConnWriteF(session->client.conn, " {%lu}\r\n", resultHeaderLen + 2) != -1) { if (ConnWrite(session->client.conn, resultHeader, resultHeaderLen) != -1) { - if (ConnWrite(session->client.conn, "\r\n", 2) != -1) { + if (ConnWrite(session->client.conn, "\r\n", 2) != -1) { MemFree(resultHeader); return(STATUS_CONTINUE); } @@ -1706,8 +1728,8 @@ FetchFlagResponderBodyHeaderFields(void *param1, void *param2, void *param3) __inline static BOOL HasPart(GArray *mimeInfo, BodyPartRequestStruct *request) { - unsigned char type[MIME_TYPE_LEN+1]; - unsigned char dummy[MIME_NAME_LEN+1]; + char type[MIME_TYPE_LEN+1]; + char dummy[MIME_NAME_LEN+1]; unsigned long dummyLong; long depthChange = 0; @@ -1723,18 +1745,18 @@ HasPart(GArray *mimeInfo, BodyPartRequestStruct *request) char *mime_string = g_array_index(mimeInfo, char *, mimeResponseLine); switch (atol(mime_string)) { case 2002: { - ParseMIMEDLine(mime_string + 5, - type, MIME_TYPE_LEN, - dummy, MIME_NAME_LEN, - dummy, MIME_NAME_LEN, - dummy, MIME_NAME_LEN, - dummy, MIME_NAME_LEN, - &(request->mimeOffset), - &(request->mimeLen), - &(request->partOffset), - &(request->partLen), - &(request->messageHeaderLen), - &dummyLong); + ParseMIMEDLine(mime_string + 5, + type, MIME_TYPE_LEN, + dummy, MIME_NAME_LEN, + dummy, MIME_NAME_LEN, + dummy, MIME_NAME_LEN, + dummy, MIME_NAME_LEN, + &(request->mimeOffset), + &(request->mimeLen), + &(request->partOffset), + &(request->partLen), + &(request->messageHeaderLen), + &dummyLong); if (toupper(type[0]) == 'M') { if (XplStrCaseCmp(type, "MULTIPART") == 0) { @@ -1782,7 +1804,7 @@ HasPart(GArray *mimeInfo, BodyPartRequestStruct *request) return(TRUE); } - + matchDepthPart = 0; } else if (matchDepthPart > request->part[matchDepth]) { /* Not Found! Send Nothing */ @@ -1820,10 +1842,10 @@ HasPart(GArray *mimeInfo, BodyPartRequestStruct *request) } continue; } - + currentDepth--; continue; - + } /* Not Found; Send Nothing */ @@ -1833,7 +1855,7 @@ HasPart(GArray *mimeInfo, BodyPartRequestStruct *request) return(FALSE); } -static long +static long FetchFlagResponderBodyPartNumber(void *param1, void *param2, void *param3) { ImapSession *session = (ImapSession *)param1; @@ -1852,7 +1874,7 @@ FetchFlagResponderBodyPartNumber(void *param1, void *param2, void *param3) } if (HasPart(FetchRequest->messageDetail.mimeInfo, &(flag->bodyPart))) { - if (NMAPSendCommandF(session->store.conn, "READ %llx %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.partOffset, flag->bodyPart.partLen) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.partOffset, flag->bodyPart.partLen) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { if (ConnWriteF(session->client.conn, "] {%lu}\r\n", (long)count) != -1) { if (ConnReadToConn(session->store.conn, session->client.conn, count) == (int)count) { @@ -1864,7 +1886,7 @@ FetchFlagResponderBodyPartNumber(void *param1, void *param2, void *param3) return(STATUS_ABORT); } - + return(CheckForNMAPCommError(ccode)); } @@ -1878,7 +1900,7 @@ FetchFlagResponderBodyPartNumber(void *param1, void *param2, void *param3) return(STATUS_ABORT); } -static long +static long FetchFlagResponderBodyPartNumberPartial(void *param1, void *param2, void *param3) { ImapSession *session = (ImapSession *)param1; @@ -1907,7 +1929,7 @@ FetchFlagResponderBodyPartNumberPartial(void *param1, void *param2, void *param3 resultLen = flag->bodyPart.partLen - flag->partial.start; } - if (NMAPSendCommandF(session->store.conn, "READ %llx %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.partOffset + flag->partial.start, resultLen) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.partOffset + flag->partial.start, resultLen) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { if (ConnWriteF(session->client.conn, "]<%lu> {%lu}\r\n", flag->partial.start, (long)count) != -1) { if (ConnReadToConn(session->store.conn, session->client.conn, count) == (int)count) { @@ -1937,7 +1959,7 @@ FetchFlagResponderBodyPartNumberPartial(void *param1, void *param2, void *param3 return(STATUS_ABORT); } -static long +static long FetchFlagResponderBodyPartText(void *param1, void *param2, void *param3) { ImapSession *session = (ImapSession *)param1; @@ -1954,9 +1976,9 @@ FetchFlagResponderBodyPartText(void *param1, void *param2, void *param3) } return(STATUS_ABORT); } - + if (HasPart(FetchRequest->messageDetail.mimeInfo, &(flag->bodyPart)) && flag->bodyPart.isMessage) { - if (NMAPSendCommandF(session->store.conn, "READ %llx %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.partOffset + flag->bodyPart.messageHeaderLen, flag->bodyPart.partLen - flag->bodyPart.messageHeaderLen) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.partOffset + flag->bodyPart.messageHeaderLen, flag->bodyPart.partLen - flag->bodyPart.messageHeaderLen) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { if (ConnWriteF(session->client.conn, ".TEXT] {%lu}\r\n", (long)count) != -1) { if (ConnReadToConn(session->store.conn, session->client.conn, count) == (int)count) { @@ -1967,7 +1989,7 @@ FetchFlagResponderBodyPartText(void *param1, void *param2, void *param3) } return(STATUS_ABORT); } - + return(CheckForNMAPCommError(ccode)); } @@ -1982,7 +2004,7 @@ FetchFlagResponderBodyPartText(void *param1, void *param2, void *param3) return(STATUS_ABORT); } -static long +static long FetchFlagResponderBodyPartTextPartial(void *param1, void *param2, void *param3) { ImapSession *session = (ImapSession *)param1; @@ -2015,7 +2037,7 @@ FetchFlagResponderBodyPartTextPartial(void *param1, void *param2, void *param3) /* len out of range */ resultLen = textLen - flag->partial.start; } - if (NMAPSendCommandF(session->store.conn, "READ %llx %lu %lu\r\n", FetchRequest->message->guid, textStart + flag->partial.start, resultLen) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu %lu\r\n", FetchRequest->message->guid, textStart + flag->partial.start, resultLen) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { if (ConnWriteF(session->client.conn, ".TEXT]<%lu> {%lu}\r\n", flag->partial.start, (long)count) != -1) { if (ConnReadToConn(session->store.conn, session->client.conn, count) == (int)count) { @@ -2044,7 +2066,7 @@ FetchFlagResponderBodyPartTextPartial(void *param1, void *param2, void *param3) return(STATUS_ABORT); } -static long +static long FetchFlagResponderBodyPartHeader(void *param1, void *param2, void *param3) { ImapSession *session = (ImapSession *)param1; @@ -2063,7 +2085,7 @@ FetchFlagResponderBodyPartHeader(void *param1, void *param2, void *param3) } if (HasPart(FetchRequest->messageDetail.mimeInfo, &(flag->bodyPart)) && flag->bodyPart.isMessage ) { - if (NMAPSendCommandF(session->store.conn, "READ %llx %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.partOffset, flag->bodyPart.messageHeaderLen) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.partOffset, flag->bodyPart.messageHeaderLen) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { if (ConnWriteF(session->client.conn, ".HEADER] {%lu}\r\n", (long)count) != -1) { if (ConnReadToConn(session->store.conn, session->client.conn, count) == (int)count) { @@ -2088,7 +2110,7 @@ FetchFlagResponderBodyPartHeader(void *param1, void *param2, void *param3) return(STATUS_ABORT); } -static long +static long FetchFlagResponderBodyPartHeaderPartial(void *param1, void *param2, void *param3) { ImapSession *session = (ImapSession *)param1; @@ -2115,7 +2137,7 @@ FetchFlagResponderBodyPartHeaderPartial(void *param1, void *param2, void *param3 /* len out of range */ resultLen = flag->bodyPart.messageHeaderLen - flag->partial.start; } - if (NMAPSendCommandF(session->store.conn, "READ %llx %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.partOffset + flag->partial.start, resultLen) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.partOffset + flag->partial.start, resultLen) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { if (ConnWriteF(session->client.conn, ".HEADER]<%lu> {%lu}\r\n", flag->partial.start, (long)count) != -1) { if (ConnReadToConn(session->store.conn, session->client.conn, count) == (int)count) { @@ -2139,18 +2161,18 @@ FetchFlagResponderBodyPartHeaderPartial(void *param1, void *param2, void *param3 return(STATUS_ABORT); } -static long +static long FetchFlagResponderBodyPartHeaderFields(void *param1, void *param2, void *param3) { FetchFlagStruct *flag = (FetchFlagStruct *)param3; ImapSession *session = (ImapSession *)param1; FetchStruct *FetchRequest = (FetchStruct *)param2; - + long ccode; unsigned long j; - unsigned char *fullHeader; + char *fullHeader; long fullHeaderLen; - unsigned char *resultHeader; + char *resultHeader; unsigned long resultHeaderLen; if (ConnWriteF(session->client.conn, "BODY[%lu", flag->bodyPart.part[0]) != -1) { @@ -2176,7 +2198,7 @@ FetchFlagResponderBodyPartHeaderFields(void *param1, void *param2, void *param3) continue; } return(STATUS_ABORT); - } + } } else { return(STATUS_ABORT); } @@ -2188,7 +2210,7 @@ FetchFlagResponderBodyPartHeaderFields(void *param1, void *param2, void *param3) } if (HasPart(FetchRequest->messageDetail.mimeInfo, &(flag->bodyPart)) && flag->bodyPart.isMessage) { - if (NMAPSendCommandF(session->store.conn, "READ %llx %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.partOffset, flag->bodyPart.messageHeaderLen) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.partOffset, flag->bodyPart.messageHeaderLen) != -1) { ccode = NMAPReadResponse(session->store.conn, session->store.response, sizeof(session->store.response), TRUE); if (ccode == 2001) { fullHeaderLen = atoi(session->store.response); @@ -2252,7 +2274,7 @@ FetchFlagResponderBodyPartHeaderFields(void *param1, void *param2, void *param3) MemFree(resultHeader); return(STATUS_ABORT); } - + return(STATUS_MEMORY_ERROR); } } @@ -2280,7 +2302,7 @@ FetchFlagResponderBodyPartHeaderFields(void *param1, void *param2, void *param3) return(STATUS_ABORT); } -static long +static long FetchFlagResponderBodyPartMime(void *param1, void *param2, void *param3) { ImapSession *session = (ImapSession *)param1; @@ -2298,7 +2320,7 @@ FetchFlagResponderBodyPartMime(void *param1, void *param2, void *param3) return(STATUS_ABORT); } if (HasPart(FetchRequest->messageDetail.mimeInfo, &(flag->bodyPart))) { - if (NMAPSendCommandF(session->store.conn, "READ %llx %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.mimeOffset, flag->bodyPart.mimeLen) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.mimeOffset, flag->bodyPart.mimeLen) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { if (ConnWriteF(session->client.conn, ".MIME] {%lu}\r\n", (long)count) != -1) { if (ConnReadToConn(session->store.conn, session->client.conn, count) == (int)count) { @@ -2322,7 +2344,7 @@ FetchFlagResponderBodyPartMime(void *param1, void *param2, void *param3) } -static long +static long FetchFlagResponderBodyPartMimePartial(void *param1, void *param2, void *param3) { ImapSession *session = (ImapSession *)param1; @@ -2345,7 +2367,7 @@ FetchFlagResponderBodyPartMimePartial(void *param1, void *param2, void *param3) } else { count = flag->bodyPart.mimeLen - flag->partial.start; } - if (NMAPSendCommandF(session->store.conn, "READ %llx %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.mimeOffset + flag->partial.start, (long)count) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " %lu %lu\r\n", FetchRequest->message->guid, flag->bodyPart.mimeOffset + flag->partial.start, (long)count) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { if (ConnWriteF(session->client.conn, ".MIME] {%lu}\r\n", (long)count) != -1) { if (ConnReadToConn(session->store.conn, session->client.conn, count) == (int)count) { @@ -2369,7 +2391,7 @@ FetchFlagResponderBodyPartMimePartial(void *param1, void *param2, void *param3) } -static long +static long FetchFlagResponderBodyPart(void *param1, void *param2, void *param3) { FetchFlagStruct *flag = (FetchFlagStruct *)param3; @@ -2379,7 +2401,7 @@ FetchFlagResponderBodyPart(void *param1, void *param2, void *param3) static long FetchFlagResponderFast(void *param1, void *param2, void *param3) { - ImapSession *session = (ImapSession *)param1; + ImapSession *session = (ImapSession *)param1; long ccode; if ((ccode = FetchFlagResponderFlags(param1, param2, param3)) == STATUS_CONTINUE) { @@ -2394,10 +2416,10 @@ FetchFlagResponderFast(void *param1, void *param2, void *param3) return(ccode); } -static long +static long FetchFlagResponderFull(void *param1, void *param2, void *param3) { - ImapSession *session = (ImapSession *)param1; + ImapSession *session = (ImapSession *)param1; long ccode; if ((ccode = FetchFlagResponderFlags(param1, param2, param3)) == STATUS_CONTINUE) { @@ -2420,10 +2442,10 @@ FetchFlagResponderFull(void *param1, void *param2, void *param3) return(ccode); } -static long +static long FetchFlagResponderAll(void *param1, void *param2, void *param3) { - ImapSession *session = (ImapSession *)param1; + ImapSession *session = (ImapSession *)param1; long ccode; if ((ccode = FetchFlagResponderFlags(param1, param2, param3)) == STATUS_CONTINUE) { @@ -2443,9 +2465,9 @@ FetchFlagResponderAll(void *param1, void *param2, void *param3) } static unsigned long -ParseFetchFlagPartial(unsigned char **parentPtr, FetchFlagStruct *flag) +ParseFetchFlagPartial(char **parentPtr, FetchFlagStruct *flag) { - unsigned char *ptr; + char *ptr; ptr = *parentPtr; if (isdigit(*ptr)) { @@ -2478,12 +2500,12 @@ ParseFetchFlagPartial(unsigned char **parentPtr, FetchFlagStruct *flag) } __inline static unsigned long -ParseHeaderFields(unsigned char **parentPtr, FetchFlagStruct *flag) +ParseHeaderFields(char **parentPtr, FetchFlagStruct *flag) { - unsigned char *start; - unsigned char *end; - unsigned char *current; - unsigned char *fieldBuffer; + char *start; + char *end; + char *current; + char *fieldBuffer; unsigned long fieldBufferLen; unsigned long spaceCount; unsigned char len; @@ -2509,7 +2531,7 @@ ParseHeaderFields(unsigned char **parentPtr, FetchFlagStruct *flag) current++; continue; } - + spaceCount++; current++; } while (current < end); @@ -2518,7 +2540,7 @@ ParseHeaderFields(unsigned char **parentPtr, FetchFlagStruct *flag) if (spaceCount < FETCH_FIELD_ALLOC_THRESHOLD) { flag->fieldList.field = &(flag->fieldList.fieldEmbedded[0]); } else { - flag->fieldList.field = MemCalloc((spaceCount + 1), sizeof(unsigned char *)); + flag->fieldList.field = MemCalloc((spaceCount + 1), sizeof(char *)); if (flag->fieldList.field) { flag->hasAllocated |= ALLOCATED_FIELD_POINTERS; } else { @@ -2564,7 +2586,7 @@ ParseHeaderFields(unsigned char **parentPtr, FetchFlagStruct *flag) } len = current - start; - + break; } while (TRUE); } @@ -2584,10 +2606,10 @@ ParseHeaderFields(unsigned char **parentPtr, FetchFlagStruct *flag) return(F_PARSE_ERROR); } - + break; } - + return(F_PARSE_ERROR); } while (current < end); @@ -2610,7 +2632,7 @@ ParseHeaderFields(unsigned char **parentPtr, FetchFlagStruct *flag) return(ccode); } - + return(F_PARSE_ERROR); } return(F_SYSTEM_ERROR); @@ -2620,13 +2642,13 @@ ParseHeaderFields(unsigned char **parentPtr, FetchFlagStruct *flag) } __inline static unsigned long -ParseFetchFlagHeaderFields(unsigned char **parentPtr, FetchFlagStruct *flag) +ParseFetchFlagHeaderFields(char **parentPtr, FetchFlagStruct *flag) { return(ParseHeaderFields(parentPtr, flag)); } __inline static unsigned long -ParseFetchFlagHeaderFieldsNot(unsigned char **parentPtr, FetchFlagStruct *flag) +ParseFetchFlagHeaderFieldsNot(char **parentPtr, FetchFlagStruct *flag) { flag->fieldList.skip = TRUE; return(ParseHeaderFields(parentPtr, flag)); @@ -2645,9 +2667,9 @@ static FetchFlag FetchFlagsSection[] = { }; static unsigned long -ParseFetchFlagBodyPart(unsigned char **parentPtr, FetchFlagStruct *flag) +ParseFetchFlagBodyPart(char **parentPtr, FetchFlagStruct *flag) { - unsigned char *ptr; + char *ptr; unsigned long *tmpPart; long sectionId; unsigned long ccode; @@ -2692,7 +2714,7 @@ ParseFetchFlagBodyPart(unsigned char **parentPtr, FetchFlagStruct *flag) ptr++; continue; } - + if (*ptr == ']') { ptr++; if (*ptr != '<') { @@ -2711,7 +2733,7 @@ ParseFetchFlagBodyPart(unsigned char **parentPtr, FetchFlagStruct *flag) return(ccode); } - + return(F_PARSE_ERROR); } @@ -2729,7 +2751,7 @@ ParseFetchFlagBodyPart(unsigned char **parentPtr, FetchFlagStruct *flag) *parentPtr = ptr; return(0); } - + return(ccode); } @@ -2836,9 +2858,9 @@ AddNewFlag(FetchStruct *FetchRequest, FetchResponder responder) } __inline static long -ParseFetchArguments(ImapSession *session, unsigned char *ptr, FetchStruct *FetchRequest) +ParseFetchArguments(ImapSession *session, char *ptr, FetchStruct *FetchRequest) { - unsigned char *Items; + char *Items; long allFlags = FetchRequest->flags; long flagID; FetchFlag flagInfo; @@ -2875,7 +2897,7 @@ ParseFetchArguments(ImapSession *session, unsigned char *ptr, FetchStruct *Fetch } continue; } - + allFlags |= flagInfo.value; ccode = flagInfo.parser(&ptr, currentFlag); @@ -2906,7 +2928,7 @@ ParseFetchArguments(ImapSession *session, unsigned char *ptr, FetchStruct *Fetch flagID = BongoKeywordBegins(FetchFlagsSoloIndex, ptr); if (flagID != -1) { flagInfo = FetchFlagsSolo[flagID]; - + ptr += flagInfo.nameLen; currentFlag = AddNewFlag(FetchRequest, flagInfo.responder); if (currentFlag) { @@ -2923,7 +2945,7 @@ ParseFetchArguments(ImapSession *session, unsigned char *ptr, FetchStruct *Fetch return(F_PARSE_ERROR); } - + allFlags |= flagInfo.value; ccode = flagInfo.parser(&ptr, currentFlag); @@ -2954,17 +2976,15 @@ __inline static long SetMessageSeenFlag(ImapSession *session, FetchStruct *FetchRequest) { long ccode; - unsigned long oldFlags; unsigned long newFlags; char *ptr; - if (NMAPSendCommandF(session->store.conn, "FLAG %llx +%lu\r\n", FetchRequest->message->guid, (unsigned long)STORE_MSG_FLAG_SEEN) != -1) { + if (NMAPSendCommandF(session->store.conn, "FLAG %" PRIx64 " +%lu\r\n", FetchRequest->message->guid, (unsigned long)STORE_MSG_FLAG_SEEN) != -1) { ccode = NMAPReadResponse(session->store.conn, session->store.response, sizeof(session->store.response), TRUE); if (ccode == 1000) { if (!(FetchRequest->flags & F_FLAGS)) { /* This fetch request does allow flags responses, but we have to wait for later to send them to the client */ - oldFlags = atol(session->store.response); ptr = strchr(session->store.response, ' '); if (ptr) { newFlags = atol(ptr + 1); @@ -2983,9 +3003,9 @@ GetMessageHeader(ImapSession *session, FetchStruct *FetchRequest) { int ccode; size_t count; - + /* Request the header */ - if (NMAPSendCommandF(session->store.conn, "READ %llx 0 %lu\r\n", FetchRequest->message->guid, FetchRequest->message->headerSize) != -1) { + if (NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " 0 %lu\r\n", FetchRequest->message->guid, FetchRequest->message->headerSize) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &count)) == 2001) { FetchRequest->message->headerSize = (long)count; FetchRequest->messageDetail.header = MemMalloc(FetchRequest->message->headerSize + 1); @@ -2999,9 +3019,9 @@ GetMessageHeader(ImapSession *session, FetchStruct *FetchRequest) } } } - } + } - return(STATUS_ABORT); + return(STATUS_ABORT); } return(CheckForNMAPCommError(ccode)); @@ -3023,7 +3043,7 @@ GetMimeInfo(ImapSession *session, FetchStruct *FetchRequest) g_array_free(FetchRequest->messageDetail.mimeInfo, TRUE); } - if (NMAPSendCommandF(session->store.conn, "MIME %llx\r\n", FetchRequest->message->guid) != -1) { + if (NMAPSendCommandF(session->store.conn, "MIME %" PRIx64 "\r\n", FetchRequest->message->guid) != -1) { ccode = NMAPReadResponse(session->store.conn, session->store.response, sizeof(session->store.response), FALSE); while(ccode != 1000) { if ((ccode > 2001) && (ccode < 2005)) { @@ -3037,7 +3057,7 @@ GetMimeInfo(ImapSession *session, FetchStruct *FetchRequest) ccode = NMAPReadResponse(session->store.conn, session->store.response, sizeof(session->store.response), FALSE); continue; } - + return(CheckForNMAPCommError(ccode)); } @@ -3083,7 +3103,7 @@ __inline static long MessageDetailsGet(ImapSession *session, FetchStruct *FetchRequest) { long ccode; - + if (FetchRequest->flags & F_NEED_HEADER) { if ((ccode = GetMessageHeader(session, FetchRequest)) == 0) { ; @@ -3179,8 +3199,8 @@ ImapCommandFetch(void *param) /* rfc 2180 discourages purge notifications during the store command */ if ((ccode = EventsSend(session, STORE_EVENT_NEW | STORE_EVENT_FLAG)) == STATUS_CONTINUE) { memset(&FetchRequest, 0, sizeof(FetchRequest)); - - FetchRequest.flags = ParseFetchArguments(session, session->command.buffer + 6, &FetchRequest); + + FetchRequest.flags = ParseFetchArguments(session, session->command.buffer + 6, &FetchRequest); if (!(FetchRequest.flags & F_ERROR)) { nextRange = FetchRequest.messageSet; @@ -3204,7 +3224,7 @@ ImapCommandFetch(void *param) } if (!(FetchRequest.flags & F_SYSTEM_ERROR)) { - FreeFetchResourcesFailure(&FetchRequest); + FreeFetchResourcesFailure(&FetchRequest); return(SendError(session->client.conn, session->command.tag, "FETCH", STATUS_INVALID_ARGUMENT)); } @@ -3232,12 +3252,12 @@ ImapCommandUidFetch(void *param) if ((ccode = EventsSend(session, STORE_EVENT_NEW | STORE_EVENT_FLAG)) == STATUS_CONTINUE) { memmove(session->command.buffer, session->command.buffer + 4, strlen(session->command.buffer + 4) + 1); memset(&FetchRequest, 0, sizeof(FetchRequest)); - - /* Respond with uid even if it has not been explicitly requested */ + + /* Respond with uid even if it has not been explicitly requested */ FetchRequest.flags = F_UID; firstFlag = AddNewFlag(&FetchRequest, FetchFlagResponderUid); if (firstFlag) { - FetchRequest.flags = ParseFetchArguments(session, session->command.buffer + 6, &FetchRequest); + FetchRequest.flags = ParseFetchArguments(session, session->command.buffer + 6, &FetchRequest); if (!(FetchRequest.flags & F_ERROR)) { nextRange = FetchRequest.messageSet; @@ -3290,17 +3310,17 @@ CommandFetchCleanup(void) BOOL CommandFetchInit(void) { - BongoKeywordIndexCreateFromTable(FetchFlagsAttIndex, FetchFlagsAtt, .name, TRUE); + BongoKeywordIndexCreateFromTable(FetchFlagsAttIndex, FetchFlagsAtt, .name, TRUE); if (FetchFlagsAttIndex != NULL) { - BongoKeywordIndexCreateFromTable(FetchFlagsSoloIndex, FetchFlagsSolo, .name, TRUE); + BongoKeywordIndexCreateFromTable(FetchFlagsSoloIndex, FetchFlagsSolo, .name, TRUE); if (FetchFlagsSoloIndex != NULL) { - BongoKeywordIndexCreateFromTable(FetchFlagsSectionIndex, FetchFlagsSection, .name, TRUE); + BongoKeywordIndexCreateFromTable(FetchFlagsSectionIndex, FetchFlagsSection, .name, TRUE); if (FetchFlagsSectionIndex) { return(TRUE); } BongoKeywordIndexFree(FetchFlagsSoloIndex); } BongoKeywordIndexFree(FetchFlagsAttIndex); - } + } return(FALSE); } diff --git a/src/agents/imap/fetch.h b/src/agents/imap/fetch.h index 7641486..afff4c6 100644 --- a/src/agents/imap/fetch.h +++ b/src/agents/imap/fetch.h @@ -66,9 +66,9 @@ typedef struct { typedef struct { unsigned long count; - unsigned char **field; - unsigned char *fieldEmbedded[FETCH_FIELD_ALLOC_THRESHOLD]; - unsigned char *fieldRequest; + char **field; + char *fieldEmbedded[FETCH_FIELD_ALLOC_THRESHOLD]; + char *fieldRequest; BOOL skip; } HeaderFieldStruct; @@ -91,7 +91,7 @@ typedef struct { typedef struct { unsigned long sequenceNumber; - unsigned char *header; + char *header; GArray *mimeInfo; } MessageDetail; @@ -110,7 +110,7 @@ typedef struct { typedef struct { unsigned long flags; - unsigned char *messageSet; + char *messageSet; unsigned long flagCount; FetchFlagStruct *flag; @@ -121,10 +121,10 @@ typedef struct { MessageDetail messageDetail; } FetchStruct; -typedef unsigned long (* FetchFlagParser)(unsigned char **ptr, FetchFlagStruct *flag); +typedef unsigned long (* FetchFlagParser)(char **ptr, FetchFlagStruct *flag); typedef struct { - unsigned char *name; + const char *name; unsigned long nameLen; unsigned long value; FetchFlagParser parser; diff --git a/src/agents/imap/imapd.c b/src/agents/imap/imapd.c index 7475653..ff862a5 100644 --- a/src/agents/imap/imapd.c +++ b/src/agents/imap/imapd.c @@ -40,52 +40,59 @@ #include #include +#include #include "imapd.h" ImapGlobal Imap; +#define IMAP_PROTOCOL_COMMAND(command, handler) \ + { command, IMAP_HELP_NOT_DEFINED, sizeof(command) - 1, handler, \ + NULL, NULL, NULL, BlackCommand } + static ProtocolCommand ImapProtocolCommands[] = { - { IMAP_COMMAND_CAPABILITY, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_CAPABILITY) - 1, ImapCommandCapability, NULL, NULL }, - { IMAP_COMMAND_NOOP, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_NOOP) - 1, ImapCommandNoop, NULL, NULL }, - { IMAP_COMMAND_LOGOUT, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_LOGOUT) - 1, ImapCommandLogout, NULL, NULL }, - { IMAP_COMMAND_STARTTLS, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_STARTTLS) - 1, ImapCommandStartTls, NULL, NULL }, - { IMAP_COMMAND_AUTHENTICATE, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_AUTHENTICATE) - 1, ImapCommandAuthenticate, NULL, NULL }, - { IMAP_COMMAND_LOGIN, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_LOGIN) - 1, ImapCommandLogin, NULL, NULL }, - { IMAP_COMMAND_SELECT, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_SELECT) - 1, ImapCommandSelect, NULL, NULL }, - { IMAP_COMMAND_EXAMINE, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_EXAMINE) - 1, ImapCommandExamine, NULL, NULL }, - { IMAP_COMMAND_CREATE, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_CREATE) - 1, ImapCommandCreate, NULL, NULL }, - { IMAP_COMMAND_DELETE, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_DELETE) - 1, ImapCommandDelete, NULL, NULL }, - { IMAP_COMMAND_RENAME, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_RENAME) - 1, ImapCommandRename, NULL, NULL }, - { IMAP_COMMAND_SUBSCRIBE, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_SUBSCRIBE) - 1, ImapCommandSubscribe, NULL, NULL }, - { IMAP_COMMAND_UNSUBSCRIBE, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_UNSUBSCRIBE) - 1, ImapCommandUnsubscribe, NULL, NULL }, - { IMAP_COMMAND_LIST, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_LIST) - 1, ImapCommandList, NULL, NULL }, - { IMAP_COMMAND_LSUB, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_LSUB) - 1, ImapCommandLsub, NULL, NULL }, - { IMAP_COMMAND_STATUS, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_STATUS) - 1, ImapCommandStatus, NULL, NULL }, - { IMAP_COMMAND_APPEND, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_APPEND) - 1, ImapCommandAppend, NULL, NULL }, - { IMAP_COMMAND_CHECK, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_CHECK) - 1, ImapCommandCheck, NULL, NULL }, - { IMAP_COMMAND_CLOSE, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_CLOSE) - 1, ImapCommandClose, NULL, NULL }, - { IMAP_COMMAND_EXPUNGE, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_EXPUNGE) - 1, ImapCommandExpunge, NULL, NULL }, - { IMAP_COMMAND_SEARCH, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_SEARCH) - 1, ImapCommandSearch, NULL, NULL }, - { IMAP_COMMAND_UID_SEARCH, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_UID_SEARCH) - 1, ImapCommandUidSearch, NULL, NULL }, - { IMAP_COMMAND_FETCH, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_FETCH) - 1, ImapCommandFetch, NULL, NULL }, - { IMAP_COMMAND_UID_FETCH, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_UID_FETCH) - 1, ImapCommandUidFetch, NULL, NULL }, - { IMAP_COMMAND_STORE, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_STORE) - 1, ImapCommandStore, NULL, NULL }, - { IMAP_COMMAND_UID_STORE, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_UID_STORE) - 1, ImapCommandUidStore, NULL, NULL }, - { IMAP_COMMAND_COPY, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_COPY) - 1, ImapCommandCopy, NULL, NULL }, - { IMAP_COMMAND_UID_COPY, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_UID_COPY) - 1, ImapCommandUidCopy, NULL, NULL }, - { IMAP_COMMAND_SETACL, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_SETACL) - 1, ImapCommandSetAcl, NULL, NULL }, - { IMAP_COMMAND_DELETEACL, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_DELETEACL) - 1, ImapCommandDeleteAcl, NULL, NULL }, - { IMAP_COMMAND_GETACL, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_GETACL) - 1, ImapCommandGetAcl, NULL, NULL }, - { IMAP_COMMAND_LISTRIGHTS, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_LISTRIGHTS) - 1, ImapCommandListRights, NULL, NULL }, - { IMAP_COMMAND_MYRIGHTS, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_MYRIGHTS) - 1, ImapCommandMyRights, NULL, NULL }, - { IMAP_COMMAND_SETQUOTA, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_SETQUOTA) - 1, ImapCommandSetQuota, NULL, NULL }, - { IMAP_COMMAND_GETQUOTA, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_GETQUOTA) - 1, ImapCommandGetQuota, NULL, NULL }, - { IMAP_COMMAND_GETQUOTAROOT, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_GETQUOTAROOT) - 1, ImapCommandGetQuotaRoot, NULL, NULL }, - { IMAP_COMMAND_NAMESPACE, IMAP_HELP_NOT_DEFINED, sizeof(IMAP_COMMAND_NAMESPACE) - 1, ImapCommandNameSpace, NULL, NULL }, - { NULL, NULL, 0, NULL, NULL, NULL } + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_CAPABILITY, ImapCommandCapability), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_NOOP, ImapCommandNoop), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_LOGOUT, ImapCommandLogout), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_STARTTLS, ImapCommandStartTls), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_AUTHENTICATE, ImapCommandAuthenticate), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_LOGIN, ImapCommandLogin), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_SELECT, ImapCommandSelect), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_EXAMINE, ImapCommandExamine), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_CREATE, ImapCommandCreate), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_DELETE, ImapCommandDelete), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_RENAME, ImapCommandRename), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_SUBSCRIBE, ImapCommandSubscribe), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_UNSUBSCRIBE, ImapCommandUnsubscribe), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_LIST, ImapCommandList), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_LSUB, ImapCommandLsub), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_STATUS, ImapCommandStatus), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_APPEND, ImapCommandAppend), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_CHECK, ImapCommandCheck), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_CLOSE, ImapCommandClose), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_EXPUNGE, ImapCommandExpunge), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_SEARCH, ImapCommandSearch), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_UID_SEARCH, ImapCommandUidSearch), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_FETCH, ImapCommandFetch), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_UID_FETCH, ImapCommandUidFetch), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_STORE, ImapCommandStore), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_UID_STORE, ImapCommandUidStore), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_COPY, ImapCommandCopy), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_UID_COPY, ImapCommandUidCopy), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_SETACL, ImapCommandSetAcl), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_DELETEACL, ImapCommandDeleteAcl), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_GETACL, ImapCommandGetAcl), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_LISTRIGHTS, ImapCommandListRights), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_MYRIGHTS, ImapCommandMyRights), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_SETQUOTA, ImapCommandSetQuota), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_GETQUOTA, ImapCommandGetQuota), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_GETQUOTAROOT, ImapCommandGetQuotaRoot), + IMAP_PROTOCOL_COMMAND(IMAP_COMMAND_NAMESPACE, ImapCommandNameSpace), + { NULL, NULL, 0, NULL, NULL, NULL, NULL, BlackCommand } }; +#undef IMAP_PROTOCOL_COMMAND + #undef DEBUG #define ImapSessionGet() MemPrivatePoolGetEntryDirect(IMAPConnectionPool, __FILE__, __LINE__) @@ -93,7 +100,7 @@ static ProtocolCommand ImapProtocolCommands[] = { void *IMAPConnectionPool = NULL; static void -FreeReturnValueIndex(unsigned long *index) +FreeReturnValueIndex(long *index) { MemFree(index); } @@ -190,6 +197,7 @@ IMAPShutdown(unsigned char *Arguments, unsigned char **Response, BOOL *CloseConn static BOOL IMAPSessionAllocCB(void *Buffer, void *ClientData) { + UNUSED_PARAMETER(ClientData); register ImapSession *session = (ImapSession *)Buffer; memset(session, 0, sizeof(ImapSession)); session->client.state = STATE_FRESH; @@ -199,6 +207,7 @@ IMAPSessionAllocCB(void *Buffer, void *ClientData) static BOOL IMAPSessionFreeCB(void *Buffer, void *ClientData) { + UNUSED_PARAMETER(ClientData); register ImapSession *session = (ImapSession *)Buffer; if (session->command.buffer) { @@ -212,7 +221,7 @@ static void ImapSessionReturn(ImapSession *session) { register ImapSession *s = session; - register unsigned char *command; + register char *command; if (s->command.bufferLen == BUFSIZE) { command = s->command.buffer; @@ -221,7 +230,7 @@ ImapSessionReturn(ImapSession *session) MemFree(s->command.buffer); } - command = (unsigned char *)MemMalloc(BUFSIZE + 1); + command = MemMalloc(BUFSIZE + 1); } if (command != NULL) { @@ -307,7 +316,7 @@ FolderWatchAdd(Connection *storeConn, FolderInformation *folder, const char *eve { long ccode; - if ((ccode = CheckConnResultIsNot(-1, STATUS_NMAP_COMM_ERROR, NMAPSendCommandF(storeConn, "WATCH %llx %s\r\n", folder->guid, eventString))) == STATUS_CONTINUE) { + if ((ccode = CheckConnResultIsNot(-1, STATUS_NMAP_COMM_ERROR, NMAPSendCommandF(storeConn, "WATCH %" PRIx64 " %s\r\n", folder->guid, eventString))) == STATUS_CONTINUE) { ccode = CheckStoreResponseCode(1000, NMAPReadResponse(storeConn, NULL, 0, 0)); } return(ccode); @@ -318,7 +327,7 @@ FolderWatchRemove(Connection *storeConn, FolderInformation *folder) { long ccode; - if ((ccode = CheckConnResultIsNot(-1, STATUS_NMAP_COMM_ERROR, NMAPSendCommandF(storeConn, "WATCH %llx\r\n", folder->guid))) == STATUS_CONTINUE) { + if ((ccode = CheckConnResultIsNot(-1, STATUS_NMAP_COMM_ERROR, NMAPSendCommandF(storeConn, "WATCH %" PRIx64 "\r\n", folder->guid))) == STATUS_CONTINUE) { ccode = CheckStoreResponseCode(1000, NMAPReadResponse(storeConn, NULL, 0, 0)); } return(ccode); @@ -748,7 +757,7 @@ MessageListLoad(Connection *storeConn, OpenedFolder *folder) long headerSize; if ((ccode = MessageListReset(folder)) == STATUS_CONTINUE) { - if (NMAPSendCommandF(storeConn, "LIST %llx Pnmap.mail.headersize\r\n", folder->info->guid) != -1) { + if (NMAPSendCommandF(storeConn, "LIST %" PRIx64 " Pnmap.mail.headersize\r\n", folder->info->guid) != -1) { for (;;) { ccode = NMAPReadResponse(storeConn, reply, sizeof(reply), TRUE); if (ccode == 2001) { @@ -855,7 +864,13 @@ ImapCommandCapability(void *param) ImapSession *session = (ImapSession *)param; if ((ccode = EventsSend(session, STORE_EVENT_ALL)) == STATUS_CONTINUE) { - if (ConnWrite(session->client.conn, Imap.command.capability.ssl.message, Imap.command.capability.ssl.len) != -1) { + const char *capability = session->client.conn->ssl.enable + ? Imap.command.capability.message + : Imap.command.capability.ssl.message; + int capabilityLen = session->client.conn->ssl.enable + ? Imap.command.capability.len + : Imap.command.capability.ssl.len; + if (ConnWrite(session->client.conn, capability, capabilityLen) != -1) { return(SendOk(session, "CAPABILITY")); } return(STATUS_ABORT); @@ -1201,8 +1216,8 @@ FolderGetMessageGuids(ImapSession *session, char *collectionName, uint64_t **lis char buffer[CONN_BUFSIZE]; uint64_t *guid; uint64_t *tmpList; - long guidCount; - long allocCount; + unsigned long guidCount; + unsigned long allocCount; if (NMAPSendCommandF(session->store.conn, "LIST %s\r\n", collectionName) != -1) { @@ -1271,7 +1286,7 @@ FolderConvertToHierarchyOnly(ImapSession *session, FolderInformation *folder) long ccode; uint64_t *guid; uint64_t *guidList = NULL; - long guidCount; + unsigned long guidCount; if (NMAPSendCommandF(session->store.conn, "FLAG %s +%u\r\n", folder->name.utf8, STORE_COLLECTION_FLAG_HIERARCHY_ONLY) == -1) { return(STATUS_NMAP_COMM_ERROR); @@ -1291,7 +1306,7 @@ FolderConvertToHierarchyOnly(ImapSession *session, FolderInformation *folder) guid = &guidList[0]; while(guidCount > 0) { - if (NMAPSendCommandF(session->store.conn, "PURGE %llx\r\n", *guid) != -1) { + if (NMAPSendCommandF(session->store.conn, "PURGE %" PRIx64 "\r\n", *guid) != -1) { ccode = NMAPReadResponse(session->store.conn, NULL, 0, 0); if ((ccode == 1000) || (ccode == 4220)) { guid++; @@ -1390,7 +1405,7 @@ FolderMoveMessages(ImapSession *session, char *sourcePathUtf8, char *destination long ccode; uint64_t *guidList = NULL; uint64_t *guid; - long guidCount; + unsigned long guidCount; ccode = FolderGetMessageGuids(session, sourcePathUtf8, &guidList, &guidCount); if (ccode != STATUS_CONTINUE) { @@ -1400,7 +1415,7 @@ FolderMoveMessages(ImapSession *session, char *sourcePathUtf8, char *destination guid = &guidList[0]; while(guidCount > 0) { - if (NMAPSendCommandF(session->store.conn, "MOVE %llx /mail/%s\r\n", *guid, destinationPathUtf8) != -1) { + if (NMAPSendCommandF(session->store.conn, "MOVE %" PRIx64 " /mail/%s\r\n", *guid, destinationPathUtf8) != -1) { ccode = NMAPReadResponse(session->store.conn, NULL, 0, 0); if ((ccode == 1000) || (ccode == 4220)) { guid++; @@ -1820,10 +1835,10 @@ FolderSend(ImapSession *session, const char *command, char *pattern, unsigned lo if (*wildcard == '*') { *wildcard = '\0'; - ccode = SendMatchingFolders(session, command, pattern, FALSE, FALSE); + ccode = SendMatchingFolders(session, command, pattern, skipUnsubscribed, FALSE); } else if (*wildcard == '%') { *wildcard = '\0'; - ccode = SendMatchingFolders(session, command, pattern, FALSE, TRUE); + ccode = SendMatchingFolders(session, command, pattern, skipUnsubscribed, TRUE); } else { ccode = SendSingleFolder(session, command, pattern); } @@ -1931,7 +1946,7 @@ CheckUnseen(OpenedFolder *openFolder) typedef unsigned long (* CheckResultFunction)(OpenedFolder *openFolder); typedef struct { - unsigned char *string; + const char *string; unsigned long stringLen; CheckResultFunction function; } StatusCommandOption; @@ -2071,8 +2086,8 @@ ImapCommandStatus(void *param) ImapSession *session = (ImapSession *)param; long ccode; char *ptr; - unsigned char *ptr2; - unsigned char *items; + char *ptr2; + char *items; OpenedFolder openFolder; FolderInformation *folder; FolderPath folderPath; @@ -2080,7 +2095,7 @@ ImapCommandStatus(void *param) if ((ccode = CheckState(session, STATE_AUTH)) == STATUS_CONTINUE) { if ((ccode = EventsSend(session, STORE_EVENT_ALL)) == STATUS_CONTINUE) { if ((ccode = GetPathArgument(session, session->command.buffer + strlen("STATUS"), &ptr, &folderPath, FALSE)) == STATUS_CONTINUE) { - ptr2 = (unsigned char *)ptr; + ptr2 = ptr; if ((ccode = GrabArgument(session, &ptr2, &items)) == STATUS_CONTINUE) { if ((ccode = FolderListLoad(session)) == STATUS_CONTINUE) { if ((ccode = FolderGetByName(session, folderPath.name, &folder)) == STATUS_CONTINUE) { @@ -2264,7 +2279,7 @@ WriteMessageInMailbox(Connection *clientConn, Connection *storeConn, uint64_t fo char buffer[100]; ccode = STATUS_NMAP_COMM_ERROR; - if (NMAPSendCommandF(storeConn, "WRITE %llx %u %lu T%lu\r\n", folderGuid, STORE_DOCTYPE_MAIL, size, (unsigned long)creationTime) != -1) { + if (NMAPSendCommandF(storeConn, "WRITE %" PRIx64 " %u %lu T%lu\r\n", folderGuid, STORE_DOCTYPE_MAIL, size, (unsigned long)creationTime) != -1) { if ((ccode = CheckForNMAPCommError(NMAPReadResponse(storeConn, NULL, 0, 0))) == 2002) { ccode = STATUS_ABORT; if (ConnWrite(clientConn, "+ Ready for more data\r\n", sizeof("+ Ready for more data\r\n") - 1) != -1) { @@ -2374,7 +2389,7 @@ PurgeDeletedMessages(ImapSession *session, BOOL client_response, MessageInformat continue; } - if (NMAPSendCommandF(store, "PURGE %llx\r\n", message->guid) != -1) { + if (NMAPSendCommandF(store, "PURGE %" PRIx64 "\r\n", message->guid) != -1) { ccode = NMAPReadResponse(store, NULL, 0, 0); if (ccode == 1000) { if (client_response) { @@ -3061,8 +3076,8 @@ ImapCommandGetQuota(void *param) { ImapSession *session = (ImapSession *)param; long ccode; - unsigned char *ptr; - unsigned char Reply[1024]; + char *ptr; + char Reply[1024]; unsigned long quota = 0; unsigned long used = 0; @@ -3106,11 +3121,11 @@ ImapCommandGetQuotaRoot(void *param) { ImapSession *session = (ImapSession *)param; long ccode; - unsigned char *ptr; - unsigned char Reply[1024]; + char *ptr; + char Reply[1024]; unsigned long quota = 0; unsigned long used = 0; - unsigned char *mbox; + char *mbox; if ((ccode = CheckState(session, STATE_AUTH)) == STATUS_CONTINUE) { if ((ccode = EventsSend(session, STORE_EVENT_ALL)) == STATUS_CONTINUE) { @@ -3178,7 +3193,7 @@ HandleConnection(void *param) long ccode; long commandId; ImapSession *session=(ImapSession *)param; - unsigned char *ptr; + char *ptr; ProtocolCommand *localCommand; session->progress = NULL; @@ -3196,9 +3211,9 @@ HandleConnection(void *param) /* Grab ident from command */ ptr = strchr(session->command.buffer,' '); if (ptr) { - if ((unsigned long)(ptr - (unsigned char *)session->command.buffer) < sizeof(session->command.tag)) { + if ((unsigned long)(ptr - session->command.buffer) < sizeof(session->command.tag)) { ptr[0]='\0'; - memcpy(session->command.tag, session->command.buffer, ptr - (unsigned char *)session->command.buffer + 1); + memcpy(session->command.tag, session->command.buffer, ptr - session->command.buffer + 1); } else { memcpy(session->command.tag, session->command.buffer, sizeof(session->command.tag)); session->command.tag[sizeof(session->command.tag)-1]='\0'; @@ -3384,15 +3399,15 @@ InitializeImapGlobals() Imap.server.ssl.port = 993; Imap.server.ssl.config.options = 0; Imap.server.ssl.config.options |= SSL_ALLOW_CHAIN; - Imap.server.ssl.config.options |= SSL_ALLOW_SSL3; + Imap.server.allowLegacyTLS = FALSE; Imap.server.threadGroupId = XplGetThreadGroupID(); strcpy(Imap.server.postmaster, "admin"); /* Imap.command. */ Imap.command.capability.acl.enabled = TRUE; /* FIXME: ACL ?? */ - Imap.command.capability.len = sprintf(Imap.command.capability.message, "%s\r\n", "* CAPABILITY IMAP4 IMAP4rev1 AUTH=LOGIN NAMESPACE XSENDER"); - Imap.command.capability.ssl.len = sprintf(Imap.command.capability.ssl.message, "%s\r\n", "* CAPABILITY IMAP4 IMAP4rev1 AUTH=LOGIN NAMESPACE STARTTLS XSENDER LOGINDISABLED"); + Imap.command.capability.len = sprintf(Imap.command.capability.message, "%s\r\n", "* CAPABILITY IMAP4 IMAP4rev1 AUTH=PLAIN AUTH=LOGIN NAMESPACE XSENDER"); + Imap.command.capability.ssl.len = sprintf(Imap.command.capability.ssl.message, "%s\r\n", "* CAPABILITY IMAP4 IMAP4rev1 NAMESPACE STARTTLS XSENDER LOGINDISABLED"); Imap.command.months[0] = "Jan"; Imap.command.months[1] = "Feb"; @@ -3462,6 +3477,8 @@ IMAPServer(void *unused) ImapSession *session; XplThreadID id = 0; + UNUSED_PARAMETER(unused); + XplRenameThread(XplGetThreadID(), "IMAP Server"); XplSafeIncrement(Imap.server.active); @@ -3530,6 +3547,7 @@ IMAPServer(void *unused) Imap.exiting = TRUE; oldTGID = XplSetThreadGroupID(Imap.server.threadGroupId); + UNUSED_PARAMETER(oldTGID); if (Imap.server.conn) { ConnClose(Imap.server.conn); @@ -3601,6 +3619,7 @@ IMAPServer(void *unused) static void BusyThread(void *unused) { + UNUSED_PARAMETER(unused); while (!Imap.exiting) { DoUpdate(); XplDelay(10000); @@ -3611,11 +3630,13 @@ static void IMAPSSLServer(void *unused) { ImapSession *session; - unsigned char *message; + char *message; unsigned long messageLen; XplThreadID id = 0; int ccode; Connection *conn; + + UNUSED_PARAMETER(unused); XplRenameThread(XplGetThreadID(), "IMAP SSL Server"); @@ -3716,6 +3737,7 @@ IMAPSSLServer(void *unused) static BongoConfigItem IMAPConfig[] = { { BONGO_JSON_INT, "o:port/i", &Imap.server.port }, { BONGO_JSON_INT, "o:port_ssl/i", &Imap.server.ssl.port }, + { BONGO_JSON_BOOL, "o:allow_legacy_tls/b", &Imap.server.allowLegacyTLS }, { BONGO_JSON_INT, "o:threads_max/i", &Imap.session.threads.max }, { BONGO_JSON_NULL, NULL, NULL } }; @@ -3754,6 +3776,9 @@ XplServiceMain(int argc, char *argv[]) int ccode; XplThreadID id=0; + UNUSED_PARAMETER(argc); + UNUSED_PARAMETER(argv); + if (XplSetEffectiveUser(MsgGetUnprivilegedUser()) < 0) { XplConsolePrintf("bongoimap: Could not drop to unprivileged user '%s', exiting.\n", MsgGetUnprivilegedUser()); return 1; @@ -3777,6 +3802,10 @@ XplServiceMain(int argc, char *argv[]) MsgInit(); MsgAuthInit(); + if (!BongoSaslInitialize()) { + XplConsolePrintf("IMAPD: Unable to initialize Cyrus SASL.\r\n"); + return -1; + } NMAPInitialize(); LogOpen("bongoimap"); @@ -3791,6 +3820,9 @@ XplServiceMain(int argc, char *argv[]) } if (!ServerSocketSSLInit()) { + if (Imap.server.allowLegacyTLS) { + Imap.server.ssl.config.options |= SSL_ALLOW_LEGACY_PROTOCOLS; + } Imap.server.ssl.config.certificate.file = MsgGetFile(MSGAPI_FILE_PUBKEY, NULL, 0); Imap.server.ssl.config.key.file = MsgGetFile(MSGAPI_FILE_PRIVKEY, NULL, 0); diff --git a/src/agents/imap/imapd.h b/src/agents/imap/imapd.h index d59beee..88d1916 100755 --- a/src/agents/imap/imapd.h +++ b/src/agents/imap/imapd.h @@ -20,6 +20,7 @@ ****************************************************************************/ #include +#include #include #include #include @@ -235,7 +236,7 @@ typedef struct { } FolderInformation; typedef struct { - unsigned char *buffer; + char *buffer; char *name; unsigned long nameLen; FolderInformation *object; @@ -292,7 +293,7 @@ typedef struct { typedef struct { struct { Connection *conn; /* connection to the store agent */ - unsigned char response[1024]; /* used to read store responses */ + char response[1024]; /* used to read store responses */ } store; struct { @@ -377,6 +378,7 @@ typedef struct { unsigned short port; int threadGroupId; + BOOL allowLegacyTLS; BOOL disabled; XplSemaphore mainSemaphore; XplSemaphore shutdownSemaphore; diff --git a/src/agents/imap/inline.h b/src/agents/imap/inline.h index 14e51aa..abaca55 100644 --- a/src/agents/imap/inline.h +++ b/src/agents/imap/inline.h @@ -48,10 +48,10 @@ ReadCommandLine(Connection *conn, char **buffer, unsigned long *bufferLen) } __inline static long -GrabOctet(ImapSession *session, unsigned char **Input, unsigned char **Destination) +GrabOctet(ImapSession *session, char **Input, char **Destination) { - unsigned char *start; - unsigned char *end; + char *start; + char *end; long Size; long ccode; @@ -104,11 +104,12 @@ GrabOctet(ImapSession *session, unsigned char **Input, unsigned char **Destinati } __inline static long -GrabLiteral(ImapSession *session, unsigned char **Input, unsigned char **Destination) +GrabLiteral(ImapSession *session, char **Input, char **Destination) { - unsigned char *contentBegin; + char *contentBegin; unsigned long contentLen; - unsigned char *closeQuote; + char *closeQuote; + UNUSED_PARAMETER(session) /* skip opening quote */ contentBegin = *Input + 1; @@ -131,12 +132,13 @@ GrabLiteral(ImapSession *session, unsigned char **Input, unsigned char **Destina } __inline static unsigned char -GrabGroup(ImapSession *session, unsigned char **Input, unsigned char **Destination) +GrabGroup(ImapSession *session, char **Input, char **Destination) { - unsigned char *contentBegin; + char *contentBegin; unsigned long contentLen; - unsigned char *closeParen; + char *closeParen; int nesting = 1; + UNUSED_PARAMETER(session) /* We have to look for the proper closing paren, since they can be nested */ closeParen = *Input; @@ -178,11 +180,12 @@ GrabGroup(ImapSession *session, unsigned char **Input, unsigned char **Destinati } __inline static unsigned char -GrabWord(ImapSession *session, unsigned char **Input, unsigned char **Destination, BOOL lookForParen) +GrabWord(ImapSession *session, char **Input, char **Destination, BOOL lookForParen) { long len; - unsigned char *spacePtr; - unsigned char *parenPtr; + char *spacePtr; + char *parenPtr; + UNUSED_PARAMETER(session) spacePtr = strchr((*Input), ' '); if (lookForParen) { @@ -223,9 +226,9 @@ GrabWord(ImapSession *session, unsigned char **Input, unsigned char **Destinatio } __inline static long -GrabArgumentEx(ImapSession *session, unsigned char **Input, unsigned char **Destination, BOOL lookForParen) +GrabArgumentEx(ImapSession *session, char **Input, char **Destination, BOOL lookForParen) { - unsigned char *ptr = *Input; + char *ptr = *Input; while (IsWhiteSpace(*ptr)) { ptr++; @@ -258,7 +261,7 @@ GrabArgumentEx(ImapSession *session, unsigned char **Input, unsigned char **Dest } __inline static long -GrabArgument(ImapSession *session, unsigned char **Input, unsigned char **Destination) +GrabArgument(ImapSession *session, char **Input, char **Destination) { return(GrabArgumentEx(session, Input, Destination, FALSE)); } @@ -336,7 +339,7 @@ SendError(Connection *conn, char *prefix, char *command, long errorValue) } __inline static long -SendOk(ImapSession *session, unsigned char *command) +SendOk(ImapSession *session, const char *command) { if (ConnWriteF(session->client.conn, "%s OK %s completed\r\n", session->command.tag, command) != -1) { return(STATUS_CONTINUE); @@ -520,7 +523,7 @@ CheckPathLen(char *path) __inline static void AddMboxSpaces(char *mbox) { - unsigned char *ptr = mbox; + char *ptr = mbox; while((ptr = strchr(ptr, 127)) != NULL) { *ptr++ = ' '; @@ -528,10 +531,10 @@ AddMboxSpaces(char *mbox) } __inline static long -GrabTwoArguments(ImapSession *session, unsigned char *arguments, unsigned char **argument1, unsigned char **argument2) +GrabTwoArguments(ImapSession *session, char *arguments, char **argument1, char **argument2) { long ccode; - unsigned char *ptr = arguments; + char *ptr = arguments; do { if (!IsWhiteSpace(*ptr)) { @@ -564,8 +567,7 @@ __inline static long LeadCommandLine(ImapSession *session) { long ccode; - unsigned char *tmpCommand; - unsigned long bytesRead; + char *tmpCommand; ccode = ConnReadAnswer(session->client.conn, session->command.buffer, session->command.bufferLen); if (ccode > -1) { @@ -573,7 +575,6 @@ LeadCommandLine(ImapSession *session) return(STATUS_CONTINUE); } - bytesRead = ccode; do { tmpCommand = MemRealloc(session->command.buffer, session->command.bufferLen * 2); if (tmpCommand) { @@ -750,7 +751,7 @@ __inline static long GetPathArgument(ImapSession *session, char *start, char **next, FolderPath *path, BOOL nullStringValid) { long ccode; - unsigned char *ptr; + char *ptr; ptr = FindNextArgument(start); if (ptr) { @@ -870,7 +871,7 @@ StoreMessageFlag(Connection *storeConn, uint64_t guid, char *actionString, unsig char *ptr; char buffer[30]; - if (NMAPSendCommandF(storeConn, "FLAG %llx %s%lu\r\n", guid, actionString, flags) != -1) { + if (NMAPSendCommandF(storeConn, "FLAG %" PRIx64 " %s%lu\r\n", guid, actionString, flags) != -1) { if (newFlags) { if ((ccode = CheckForNMAPCommError(NMAPReadResponse(storeConn, buffer, sizeof(buffer), TRUE))) == 1000) { if ((ptr = strchr(buffer, ' ')) != NULL) { @@ -935,4 +936,3 @@ SearchKnownMessagesByGuid(MessageInformation *messageList, unsigned long message } return(NULL); } - diff --git a/src/agents/imap/rfc822parse.c b/src/agents/imap/rfc822parse.c index 7592155..e841e3f 100644 --- a/src/agents/imap/rfc822parse.c +++ b/src/agents/imap/rfc822parse.c @@ -26,7 +26,7 @@ /* RFC822 Address parsing */ #define BROKEN_ADDRESS_HOST ".Syntax-Error." #define BROKEN_ADDRESS_MAILBOX "UnexpectedDataAfterAddress" -const unsigned char *WSpecials = " ()<>@,;:\\\"[]\1\2\3\4\5\6\7\10\11\12\13\14\15\16\17\20\21\22\23\24\25\26\27\30\31\32\33\34\35\36\37\177"; +const char *WSpecials = " ()<>@,;:\\\"[]\1\2\3\4\5\6\7\10\11\12\13\14\15\16\17\20\21\22\23\24\25\26\27\30\31\32\33\34\35\36\37\177"; #define I2C_ESC 0x1b /* ESCape */ #define I2C_MULTI 0x24 /* multi-byte character set */ @@ -40,12 +40,12 @@ const unsigned char *WSpecials = " ()<>@,;:\\\"[]\1\2\3\4\5\6\7\10\11\12\13\14\1 #define I2CS_94_ASCII 0x42 /* 4/2 ISO 646 USA (ASCII) */ #define I2CS_ASCII (I2CS_94 | I2CS_94_ASCII) -static unsigned char -*RFC822SkipComment(unsigned char **String, unsigned long Trim) +static char +*RFC822SkipComment(char **String, unsigned long Trim) { - unsigned char *RetVal; - unsigned char *ptr = *String; - unsigned char *t = NULL; + char *RetVal; + char *ptr = *String; + char *t = NULL; /* Skip past whitespace */ for (RetVal = ++ptr; (*RetVal==' ' || *RetVal==0x09); RetVal++) { @@ -103,7 +103,7 @@ static unsigned char static void -RFC822SkipWhitespace(unsigned char **String) +RFC822SkipWhitespace(char **String) { while (TRUE) { if ((**String == ' ') || (**String == 0x09)) { @@ -115,13 +115,13 @@ RFC822SkipWhitespace(unsigned char **String) } -static unsigned char -*RFC822Quote(unsigned char *src) +static char +*RFC822Quote(char *src) { - unsigned char *ret = src; + char *ret = src; if (strpbrk(src,"\\\"")) { - unsigned char *dst = ret; + char *dst = ret; while (*src) { if (*src == '\"') { @@ -139,18 +139,18 @@ static unsigned char } -static unsigned char -*RFC822Copy(unsigned char *Source) +static char +*RFC822Copy(char *Source) { return(RFC822Quote(MemStrdup(Source))); } -static unsigned char -*RFC822ParseWord(unsigned char *s, const unsigned char *Delimiters) +static char +*RFC822ParseWord(char *s, const char *Delimiters) { - unsigned char *st; - unsigned char *str; + char *st; + char *str; if (!s) { return(NULL); @@ -232,6 +232,7 @@ static unsigned char str = st + 2; break; } + __attribute__((fallthrough)); } default: { @@ -243,10 +244,10 @@ static unsigned char } -static unsigned char -*RFC822ParsePhrase(unsigned char *s) +static char +*RFC822ParsePhrase(char *s) { - unsigned char *curpos; + char *curpos; if (!s) { return(NULL); @@ -269,14 +270,14 @@ static unsigned char static RFC822AddressStruct -*RFC822ParseAddrSpec(unsigned char *Address, unsigned char **ret, unsigned char *DefaultHost) +*RFC822ParseAddrSpec(char *Address, char **ret, char *DefaultHost) { RFC822AddressStruct *Adr; - unsigned char c; - unsigned char *s; - unsigned char *t; - unsigned char *v; - unsigned char *end; + char c; + char *s; + char *t; + char *v; + char *end; if (!Address) { return(NULL); @@ -360,21 +361,21 @@ static RFC822AddressStruct } -unsigned char -*RFC822ParseDomain(unsigned char *Address, unsigned char **end) +char +*RFC822ParseDomain(char *Address, char **end) { - unsigned char *ret = NULL; - unsigned char c; - unsigned char *s; - unsigned char *t; - unsigned char *v; + char *ret = NULL; + char c; + char *s; + char *t; + char *v; RFC822SkipWhitespace(&Address); if (*Address == '[') { if ((*end=RFC822ParseWord(Address + 1,"]\\"))!=NULL) { size_t len = ++*end - Address; - ret=(unsigned char *)MemMalloc(len + 1); + ret=(char *)MemMalloc(len + 1); strncpy(ret, Address, len); ret[len]='\0'; } @@ -395,7 +396,7 @@ unsigned char s=RFC822Copy(Address); *t=c; - v=(unsigned char *)MemMalloc(strlen(ret)+strlen(s)+2); + v=(char *)MemMalloc(strlen(ret)+strlen(s)+2); if (v) { sprintf (v, "%s.%s", ret, s); MemFree(ret); @@ -413,12 +414,12 @@ unsigned char static RFC822AddressStruct -*RFC822ParseRouteAddress(unsigned char *Address, unsigned char **ret, unsigned char *DefaultHost) +*RFC822ParseRouteAddress(char *Address, char **ret, char *DefaultHost) { RFC822AddressStruct *Adr; - unsigned char *s; - unsigned char *t; - unsigned char *adl; + char *s; + char *t; + char *adl; size_t adllen, i; if (!Address) { @@ -439,7 +440,7 @@ static RFC822AddressStruct for (adl = NULL, adllen = 0; (*t == '@') && ((s = RFC822ParseDomain(t + 1, &t)) != NULL); ) { /* parse possible A-D-L */ i = strlen (s) + 2; if (adl) { - unsigned char *tmp; + char *tmp; tmp = MemRealloc(adl, adllen + i); if (tmp) { @@ -503,11 +504,11 @@ static RFC822AddressStruct static RFC822AddressStruct -*RFC822ParseMailbox(unsigned char **Address, unsigned char *DefaultHost) +*RFC822ParseMailbox(char **Address, char *DefaultHost) { RFC822AddressStruct *Adr = NULL; - unsigned char *s; - unsigned char *end; + char *s; + char *end; if (!*Address) { return(NULL); @@ -539,10 +540,10 @@ static RFC822AddressStruct static RFC822AddressStruct -*RFC822ParseGroup(RFC822AddressStruct **List, RFC822AddressStruct *Last, unsigned char **Address, char *DefaultHost, unsigned long Depth) +*RFC822ParseGroup(RFC822AddressStruct **List, RFC822AddressStruct *Last, char **Address, char *DefaultHost, unsigned long Depth) { - unsigned char *p; - unsigned char *s; + char *p; + char *s; RFC822AddressStruct *Adr; if (Depth > 50) { @@ -635,7 +636,7 @@ static RFC822AddressStruct RFC822AddressStruct -*RFC822ParseAddress(RFC822AddressStruct **List, RFC822AddressStruct *Last, unsigned char **Address, unsigned char *DefaultHost, unsigned long Depth) +*RFC822ParseAddress(RFC822AddressStruct **List, RFC822AddressStruct *Last, char **Address, char *DefaultHost, unsigned long Depth) { RFC822AddressStruct *Adr; @@ -668,11 +669,11 @@ RFC822AddressStruct BOOL -RFC822ParseAddressList(RFC822AddressStruct **List, unsigned char *Address, unsigned char *DefaultHost) +RFC822ParseAddressList(RFC822AddressStruct **List, char *Address, char *DefaultHost) { RFC822AddressStruct *Last=*List; RFC822AddressStruct *Adr; - unsigned char c; + char c; if (!Address) { return(FALSE); diff --git a/src/agents/imap/rfc822parse.h b/src/agents/imap/rfc822parse.h index b57c9e7..094ef85 100644 --- a/src/agents/imap/rfc822parse.h +++ b/src/agents/imap/rfc822parse.h @@ -20,14 +20,14 @@ ****************************************************************************/ typedef struct _RFC822AddressStruct { - unsigned char *Personal; - unsigned char *Mailbox; - unsigned char *Host; - unsigned char *ADL; + char *Personal; + char *Mailbox; + char *Host; + char *ADL; struct _RFC822AddressStruct *Next; } RFC822AddressStruct; -unsigned char *RFC822ParseDomain(unsigned char *Address, unsigned char **end); -RFC822AddressStruct *RFC822ParseAddress(RFC822AddressStruct **List, RFC822AddressStruct *Last, unsigned char **Address, unsigned char *DefaultHost, unsigned long Depth); -BOOL RFC822ParseAddressList(RFC822AddressStruct **List, unsigned char *Address, unsigned char *DefaultHost); +char *RFC822ParseDomain(char *Address, char **end); +RFC822AddressStruct *RFC822ParseAddress(RFC822AddressStruct **List, RFC822AddressStruct *Last, char **Address, char *DefaultHost, unsigned long Depth); +BOOL RFC822ParseAddressList(RFC822AddressStruct **List, char *Address, char *DefaultHost); void RFC822FreeAddressList(RFC822AddressStruct *List); diff --git a/src/agents/imap/search.c b/src/agents/imap/search.c index 5793ce3..00ad810 100644 --- a/src/agents/imap/search.c +++ b/src/agents/imap/search.c @@ -25,9 +25,9 @@ #include "search.h" #include -long SearchHandleKey(ImapSession *session, char **keyString, SearchKey *key); -static long SearchHandleSubKey(ImapSession *session, char **keyString, SearchKey *key); -typedef long (* SearchRemainingMatch)(ImapSession *session, MessageInformation *message, char *arg1, void *arg2, BOOL *result); +long SearchHandleKey(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))); +static long SearchHandleSubKey(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))); +typedef long (* SearchRemainingMatch)(ImapSession *session __attribute__((unused)), MessageInformation *message, char *arg1 __attribute__((unused)), void *arg2 __attribute__((unused)), BOOL *result); long ReadSourceFile(void *source, char *buffer, unsigned long maxRead); long ReadSourceConn(void *source, char *buffer, unsigned long maxRead); @@ -46,7 +46,7 @@ ReadSourceConn(void *source, char *buffer, unsigned long maxRead) } __inline static void -MarkedMatchesPreserve(SearchKey *key) +MarkedMatchesPreserve(SearchKey *key __attribute__((unused))) { long readNum = 0; long writeNum = 0; @@ -70,7 +70,7 @@ MarkedMatchesPreserve(SearchKey *key) } __inline static void -MarkedMatchesRemove(SearchKey *key) +MarkedMatchesRemove(SearchKey *key __attribute__((unused))) { unsigned long readNum = 0; unsigned long writeNum = 0; @@ -94,7 +94,7 @@ MarkedMatchesRemove(SearchKey *key) __inline static void -MarkMatch(SearchKey *key, unsigned long index) +MarkMatch(SearchKey *key __attribute__((unused)), unsigned long index) { /* make sure that it is not already marked */ if (key->matchList[index] > -1) { @@ -103,7 +103,7 @@ MarkMatch(SearchKey *key, unsigned long index) } __inline static void -MarkMatches(SearchKey *key, SearchKey *newKey) +MarkMatches(SearchKey *key __attribute__((unused)), SearchKey *newKey) { unsigned long readNum = 0; unsigned long writeNum = 0; @@ -128,7 +128,7 @@ MarkMatches(SearchKey *key, SearchKey *newKey) __inline static long -SearchKeyObjectDuplicate(SearchKey *key, SearchKey *newKey) +SearchKeyObjectDuplicate(SearchKey *key __attribute__((unused)), SearchKey *newKey) { memset(newKey, 0, sizeof(SearchKey)); @@ -157,7 +157,7 @@ SearchKeyObjectDuplicate(SearchKey *key, SearchKey *newKey) } __inline static long -SearchKeyObjectCreate(SearchKey *key, char *charset, unsigned long msgCount) +SearchKeyObjectCreate(SearchKey *key __attribute__((unused)), char *charset, unsigned long msgCount) { unsigned long i; unsigned long len; @@ -189,7 +189,7 @@ SearchKeyObjectCreate(SearchKey *key, char *charset, unsigned long msgCount) } __inline static void -SearchKeyObjectFree(SearchKey *key) +SearchKeyObjectFree(SearchKey *key __attribute__((unused))) { MemFree(key->matchList); @@ -199,7 +199,7 @@ SearchKeyObjectFree(SearchKey *key) } __inline static long -SearchStringToUtf8(SearchKey *key, char *in, char **out) +SearchStringToUtf8(SearchKey *key __attribute__((unused)), char *in, char **out) { char buffer[SEARCH_MAX_UTF8_STRING]; long len; @@ -224,12 +224,12 @@ SearchStringToUtf8(SearchKey *key, char *in, char **out) } __inline static long -GetSearchStringArg(ImapSession *session, SearchKey *key, char **keyString, char **utf8SearchString) +GetSearchStringArg(ImapSession *session __attribute__((unused)), SearchKey *key __attribute__((unused)), char **keyString __attribute__((unused)), char **utf8SearchString) { long ccode; char *searchString; - if((ccode = GrabArgumentEx(session, (unsigned char **)keyString, (unsigned char **)&(searchString), TRUE)) == STATUS_CONTINUE) { + if((ccode = GrabArgumentEx(session, keyString, &searchString, TRUE)) == STATUS_CONTINUE) { if (XplStrCaseCmp(key->charset, "utf-8") == 0) { *utf8SearchString = searchString; return(STATUS_CONTINUE); @@ -245,7 +245,7 @@ GetSearchStringArg(ImapSession *session, SearchKey *key, char **keyString, char } __inline static long -ImapDateArgToUtc(char *dateText, unsigned long *dateUtc) +ImapDateArgToUtc(char *dateText, time_t *dateUtc) { char *dayPtr; char *monthPtr; @@ -271,12 +271,12 @@ ImapDateArgToUtc(char *dateText, unsigned long *dateUtc) } __inline static long -GetDateArg(ImapSession *session, char **keyString, time_t *date) +GetDateArg(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), time_t *date) { long ccode; char *dateText; - if((ccode = GrabArgumentEx(session, (unsigned char **)keyString, (unsigned char **)&(dateText), TRUE)) == STATUS_CONTINUE) { + if((ccode = GrabArgumentEx(session, keyString, &dateText, TRUE)) == STATUS_CONTINUE) { ccode = ImapDateArgToUtc(dateText, date); MemFree(dateText); return(ccode); @@ -285,13 +285,13 @@ GetDateArg(ImapSession *session, char **keyString, time_t *date) } __inline static long -GetMessageSentDate(ImapSession *session, uint64_t guid, unsigned long headerLen, time_t *dateUtc) +GetMessageSentDate(ImapSession *session __attribute__((unused)), uint64_t guid, unsigned long headerLen, time_t *dateUtc) { long ccode; size_t len; char *dateString; - if ((ccode = NMAPSendCommandF(session->store.conn, "READ %llx 0 %lu\r\n", guid, headerLen)) != -1) { + if ((ccode = NMAPSendCommandF(session->store.conn, "READ %" PRIx64 " 0 %lu\r\n", guid, headerLen)) != -1) { if ((ccode = NMAPReadPropertyValueLength(session->store.conn, "nmap.document", &len)) == 2001) { if ((dateString = BongoStreamGrabRfc822Header(ReadSourceConn, session->store.conn, (unsigned long)len, "Date")) != NULL) { if (NMAPReadCrLf(session->store.conn) == 2) { @@ -309,13 +309,13 @@ GetMessageSentDate(ImapSession *session, uint64_t guid, unsigned long headerLen, } __inline static long -GetNumericArg(ImapSession *session, char **keyString, unsigned long *num) +GetNumericArg(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), unsigned long *num) { long ccode; - unsigned char *numText; + char *numText; long longNum; - if((ccode = GrabArgumentEx(session, (unsigned char **)keyString, &numText, TRUE)) == STATUS_CONTINUE) { + if((ccode = GrabArgumentEx(session, keyString, &numText, TRUE)) == STATUS_CONTINUE) { longNum = atol(numText); MemFree(numText); if (longNum >= 0) { @@ -328,10 +328,10 @@ GetNumericArg(ImapSession *session, char **keyString, unsigned long *num) } __inline static long -GetMessageSetArg(ImapSession *session, char **keyString, long **messageSet) +GetMessageSetArg(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), long **messageSet) { long ccode; - unsigned char *setText; + char *setText; char *nextRange; unsigned long start; unsigned long end; @@ -339,7 +339,7 @@ GetMessageSetArg(ImapSession *session, char **keyString, long **messageSet) unsigned long messageCount = session->folder.selected.messageCount; long *workSet; - if((ccode = GrabArgumentEx(session, (unsigned char **)keyString, &setText, TRUE)) == STATUS_CONTINUE) { + if((ccode = GrabArgumentEx(session, keyString, &setText, TRUE)) == STATUS_CONTINUE) { nextRange = setText; if ((workSet = MemMalloc0(sizeof(long) * session->folder.selected.messageCount)) != NULL) { do { @@ -371,14 +371,14 @@ GetMessageSetArg(ImapSession *session, char **keyString, long **messageSet) __inline static long -FullListNoMatch(SearchKey *key) +FullListNoMatch(SearchKey *key __attribute__((unused))) { key->matchList[0] = -1; return(STATUS_CONTINUE); } __inline static long -FullListProcessMatch(ImapSession *session, SearchKey *key, char *response) +FullListProcessMatch(ImapSession *session __attribute__((unused)), SearchKey *key __attribute__((unused)), char *response) { long ccode; char *ptr; @@ -398,11 +398,11 @@ FullListProcessMatch(ImapSession *session, SearchKey *key, char *response) } __inline static long -FullListSubstringSearch(ImapSession *session, SearchKey *key, char *searchString, char *subcommand) +FullListSubstringSearch(ImapSession *session __attribute__((unused)), SearchKey *key __attribute__((unused)), char *searchString, char *subcommand) { long ccode; - if (NMAPSendCommandF(session->store.conn, "SEARCH %llx %s \"%s\"\r\n", session->folder.selected.info->guid, subcommand, searchString) != -1) { + if (NMAPSendCommandF(session->store.conn, "SEARCH %" PRIx64 " %s \"%s\"\r\n", session->folder.selected.info->guid, subcommand, searchString) != -1) { for (;;) { ccode = NMAPReadResponse(session->store.conn, session->store.response, sizeof(session->store.response), TRUE); if (ccode == 2001) { @@ -424,7 +424,7 @@ FullListSubstringSearch(ImapSession *session, SearchKey *key, char *searchString /* SearchRemainingMatch start */ static long -SearchRemainingMatchFlag(ImapSession *session, MessageInformation *message, char *arg1, void *arg2, BOOL *found) +SearchRemainingMatchFlag(ImapSession *session __attribute__((unused)), MessageInformation *message, char *arg1 __attribute__((unused)), void *arg2 __attribute__((unused)), BOOL *found) { unsigned long flag = (unsigned long)arg2; @@ -433,7 +433,7 @@ SearchRemainingMatchFlag(ImapSession *session, MessageInformation *message, char } static long -SearchRemainingMatchFlagNot(ImapSession *session, MessageInformation *message, char *arg1, void *arg2, BOOL *found) +SearchRemainingMatchFlagNot(ImapSession *session __attribute__((unused)), MessageInformation *message, char *arg1 __attribute__((unused)), void *arg2 __attribute__((unused)), BOOL *found) { unsigned long flag = (unsigned long)arg2; @@ -442,14 +442,14 @@ SearchRemainingMatchFlagNot(ImapSession *session, MessageInformation *message, c } static long -SearchRemainingMatchNew(ImapSession *session, MessageInformation *message, char *arg1, void *arg2, BOOL *found) +SearchRemainingMatchNew(ImapSession *session __attribute__((unused)), MessageInformation *message, char *arg1 __attribute__((unused)), void *arg2 __attribute__((unused)), BOOL *found) { *found = (((message->flags & STORE_MSG_FLAG_RECENT) != 0) && ((message->flags & STORE_MSG_FLAG_SEEN) == 0)); return(STATUS_CONTINUE); } static long -SearchRemainingMatchBefore(ImapSession *session, MessageInformation *message, char *arg1, void *arg2, BOOL *found) +SearchRemainingMatchBefore(ImapSession *session __attribute__((unused)), MessageInformation *message, char *arg1 __attribute__((unused)), void *arg2 __attribute__((unused)), BOOL *found) { unsigned long date = *(unsigned long *)arg2; @@ -458,7 +458,7 @@ SearchRemainingMatchBefore(ImapSession *session, MessageInformation *message, ch } static long -SearchRemainingMatchOn(ImapSession *session, MessageInformation *message, char *arg1, void *arg2, BOOL *found) +SearchRemainingMatchOn(ImapSession *session __attribute__((unused)), MessageInformation *message, char *arg1 __attribute__((unused)), void *arg2 __attribute__((unused)), BOOL *found) { unsigned long date = *(unsigned long *)arg2; @@ -467,7 +467,7 @@ SearchRemainingMatchOn(ImapSession *session, MessageInformation *message, char * } static long -SearchRemainingMatchSince(ImapSession *session, MessageInformation *message, char *arg1, void *arg2, BOOL *found) +SearchRemainingMatchSince(ImapSession *session __attribute__((unused)), MessageInformation *message, char *arg1 __attribute__((unused)), void *arg2 __attribute__((unused)), BOOL *found) { unsigned long date = *(unsigned long *)arg2; @@ -476,7 +476,7 @@ SearchRemainingMatchSince(ImapSession *session, MessageInformation *message, cha } static long -SearchRemainingMatchSentBefore(ImapSession *session, MessageInformation *message, char *arg1, void *arg2, BOOL *found) +SearchRemainingMatchSentBefore(ImapSession *session __attribute__((unused)), MessageInformation *message, char *arg1 __attribute__((unused)), void *arg2 __attribute__((unused)), BOOL *found) { long ccode; time_t date = *(unsigned long *)arg2; @@ -490,7 +490,7 @@ SearchRemainingMatchSentBefore(ImapSession *session, MessageInformation *message } static long -SearchRemainingMatchSentOn(ImapSession *session, MessageInformation *message, char *arg1, void *arg2, BOOL *found) +SearchRemainingMatchSentOn(ImapSession *session __attribute__((unused)), MessageInformation *message, char *arg1 __attribute__((unused)), void *arg2 __attribute__((unused)), BOOL *found) { long ccode; time_t date = *(unsigned long *)arg2; @@ -504,7 +504,7 @@ SearchRemainingMatchSentOn(ImapSession *session, MessageInformation *message, ch } static long -SearchRemainingMatchSentSince(ImapSession *session, MessageInformation *message, char *arg1, void *arg2, BOOL *found) +SearchRemainingMatchSentSince(ImapSession *session __attribute__((unused)), MessageInformation *message, char *arg1 __attribute__((unused)), void *arg2 __attribute__((unused)), BOOL *found) { long ccode; time_t date = *(unsigned long *)arg2; @@ -518,7 +518,7 @@ SearchRemainingMatchSentSince(ImapSession *session, MessageInformation *message, } static long -SearchRemainingMatchLarger(ImapSession *session, MessageInformation *message, char *arg1, void *arg2, BOOL *found) +SearchRemainingMatchLarger(ImapSession *session __attribute__((unused)), MessageInformation *message, char *arg1 __attribute__((unused)), void *arg2 __attribute__((unused)), BOOL *found) { unsigned long size = (unsigned long)arg2; @@ -527,7 +527,7 @@ SearchRemainingMatchLarger(ImapSession *session, MessageInformation *message, ch } static long -SearchRemainingMatchSmaller(ImapSession *session, MessageInformation *message, char *arg1, void *arg2, BOOL *found) +SearchRemainingMatchSmaller(ImapSession *session __attribute__((unused)), MessageInformation *message, char *arg1 __attribute__((unused)), void *arg2 __attribute__((unused)), BOOL *found) { unsigned long size = (unsigned long)arg2; @@ -536,12 +536,12 @@ SearchRemainingMatchSmaller(ImapSession *session, MessageInformation *message, c } static long -SearchRemainingMatchSubstring(ImapSession *session, MessageInformation *message, char *subcommand, void *arg2, BOOL *found) +SearchRemainingMatchSubstring(ImapSession *session __attribute__((unused)), MessageInformation *message, char *subcommand, void *arg2 __attribute__((unused)), BOOL *found) { char *searchString = (char *)arg2; long ccode; - if (NMAPSendCommandF(session->store.conn, "SEARCH %llx %s \"%s\"\r\n", message->guid, subcommand, searchString) != -1) { + if (NMAPSendCommandF(session->store.conn, "SEARCH %" PRIx64 " %s \"%s\"\r\n", message->guid, subcommand, searchString) != -1) { if ((ccode = NMAPReadResponse(session->store.conn, session->store.response, sizeof(session->store.response), TRUE)) == 1000) { *found = FALSE; return(STATUS_CONTINUE); @@ -558,7 +558,7 @@ SearchRemainingMatchSubstring(ImapSession *session, MessageInformation *message, __inline static long -SearchRemainingMatches(ImapSession *session, SearchKey *key, SearchRemainingMatch searchRemainingFunction, char *arg1, void *arg2) +SearchRemainingMatches(ImapSession *session __attribute__((unused)), SearchKey *key __attribute__((unused)), SearchRemainingMatch searchRemainingFunction, char *arg1 __attribute__((unused)), void *arg2 __attribute__((unused))) { MessageInformation *message = &session->folder.selected.message[0]; long ccode; @@ -597,7 +597,7 @@ SearchRemainingMatches(ImapSession *session, SearchKey *key, SearchRemainingMatc __inline static long -MessageSetAnd(ImapSession *session, SearchKey *key, long *messageSet) +MessageSetAnd(ImapSession *session __attribute__((unused)), SearchKey *key __attribute__((unused)), long *messageSet) { long readNum = 0; long writeNum = 0; @@ -624,7 +624,7 @@ MessageSetAnd(ImapSession *session, SearchKey *key, long *messageSet) __inline static long -SubStringSearch(ImapSession *session, char **keyString, SearchKey *key, char *subcommand) +SubStringSearch(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused)), char *subcommand) { long ccode; char *utf8SearchString; @@ -645,19 +645,19 @@ SubStringSearch(ImapSession *session, char **keyString, SearchKey *key, char *su /* key handlers begin */ static long -SearchKeyHandlerAll(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerAll(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(STATUS_CONTINUE); } static long -SearchKeyHandlerAnswered(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerAnswered(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SearchRemainingMatches(session, key, SearchRemainingMatchFlag, NULL, (void *)STORE_MSG_FLAG_ANSWERED)); } static long -SearchKeyHandlerBcc(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerBcc(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { long ccode; char *utf8SearchString; @@ -671,7 +671,7 @@ SearchKeyHandlerBcc(ImapSession *session, char **keyString, SearchKey *key) } static long -SearchKeyHandlerBefore(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerBefore(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { time_t date; long ccode; @@ -684,49 +684,49 @@ SearchKeyHandlerBefore(ImapSession *session, char **keyString, SearchKey *key) static long -SearchKeyHandlerBody(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerBody(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SubStringSearch(session, keyString, key, "BODY")); } static long -SearchKeyHandlerCc(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerCc(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SubStringSearch(session, keyString, key, "HEADER CC")); } static long -SearchKeyHandlerDeleted(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerDeleted(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SearchRemainingMatches(session, key, SearchRemainingMatchFlag, NULL, (void *)STORE_MSG_FLAG_DELETED)); } static long -SearchKeyHandlerDraft(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerDraft(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SearchRemainingMatches(session, key, SearchRemainingMatchFlag, NULL, (void *)STORE_MSG_FLAG_DRAFT)); } static long -SearchKeyHandlerFlagged(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerFlagged(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SearchRemainingMatches(session, key, SearchRemainingMatchFlag, NULL, (void *)STORE_MSG_FLAG_FLAGGED)); } static long -SearchKeyHandlerFrom(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerFrom(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SubStringSearch(session, keyString, key, "HEADER FROM")); } static long -SearchKeyHandlerHeader(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerHeader(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { long ccode; - unsigned char *headerName; + char *headerName; char substring[SEARCH_MAX_UTF8_STRING]; - if((ccode = GrabArgumentEx(session, (unsigned char **)keyString, &headerName, TRUE)) == STATUS_CONTINUE) { + if((ccode = GrabArgumentEx(session, keyString, &headerName, TRUE)) == STATUS_CONTINUE) { snprintf(substring, sizeof(substring), "HEADER %s", headerName); MemFree(headerName); return(SubStringSearch(session, keyString, key, substring)); @@ -735,7 +735,7 @@ SearchKeyHandlerHeader(ImapSession *session, char **keyString, SearchKey *key) } static long -SearchKeyHandlerKeyword(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerKeyword(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { long ccode; char *utf8SearchString; @@ -749,7 +749,7 @@ SearchKeyHandlerKeyword(ImapSession *session, char **keyString, SearchKey *key) } static long -SearchKeyHandlerLarger(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerLarger(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { unsigned long size; long ccode; @@ -761,13 +761,13 @@ SearchKeyHandlerLarger(ImapSession *session, char **keyString, SearchKey *key) } static long -SearchKeyHandlerNew(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerNew(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SearchRemainingMatches(session, key, SearchRemainingMatchNew, NULL, NULL)); } static long -SearchKeyHandlerNot(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerNot(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { long ccode; SearchKey newKey; @@ -783,13 +783,13 @@ SearchKeyHandlerNot(ImapSession *session, char **keyString, SearchKey *key) } static long -SearchKeyHandlerOld(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerOld(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SearchRemainingMatches(session, key, SearchRemainingMatchFlagNot, NULL, (void *)STORE_MSG_FLAG_RECENT)); } static long -SearchKeyHandlerOn(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerOn(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { time_t date; long ccode; @@ -801,7 +801,7 @@ SearchKeyHandlerOn(ImapSession *session, char **keyString, SearchKey *key) } static long -SearchKeyHandlerOr(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerOr(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { long ccode; SearchKey leftKey; @@ -829,19 +829,19 @@ SearchKeyHandlerOr(ImapSession *session, char **keyString, SearchKey *key) } static long -SearchKeyHandlerRecent(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerRecent(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SearchRemainingMatches(session, key, SearchRemainingMatchFlag, NULL, (void *)STORE_MSG_FLAG_RECENT)); } static long -SearchKeyHandlerSeen(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerSeen(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SearchRemainingMatches(session, key, SearchRemainingMatchFlag, NULL, (void *)STORE_MSG_FLAG_SEEN)); } static long -SearchKeyHandlerSentBefore(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerSentBefore(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { time_t date; long ccode; @@ -853,7 +853,7 @@ SearchKeyHandlerSentBefore(ImapSession *session, char **keyString, SearchKey *ke } static long -SearchKeyHandlerSentOn(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerSentOn(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { time_t date; long ccode; @@ -865,7 +865,7 @@ SearchKeyHandlerSentOn(ImapSession *session, char **keyString, SearchKey *key) } static long -SearchKeyHandlerSentSince(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerSentSince(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { time_t date; long ccode; @@ -877,7 +877,7 @@ SearchKeyHandlerSentSince(ImapSession *session, char **keyString, SearchKey *key } static long -SearchKeyHandlerSince(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerSince(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { time_t date; long ccode; @@ -889,7 +889,7 @@ SearchKeyHandlerSince(ImapSession *session, char **keyString, SearchKey *key) } static long -SearchKeyHandlerSmaller(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerSmaller(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { unsigned long size; long ccode; @@ -901,25 +901,25 @@ SearchKeyHandlerSmaller(ImapSession *session, char **keyString, SearchKey *key) } static long -SearchKeyHandlerSubject(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerSubject(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SubStringSearch(session, keyString, key, "HEADER SUBJECT")); } static long -SearchKeyHandlerText(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerText(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SubStringSearch(session, keyString, key, "TEXT")); } static long -SearchKeyHandlerTo(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerTo(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SubStringSearch(session, keyString, key, "HEADER TO")); } static long -SearchKeyHandlerUid(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerUid(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { long ccode; long *messageSet; @@ -932,31 +932,31 @@ SearchKeyHandlerUid(ImapSession *session, char **keyString, SearchKey *key) } static long -SearchKeyHandlerUnanswered(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerUnanswered(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SearchRemainingMatches(session, key, SearchRemainingMatchFlagNot, NULL, (void *)STORE_MSG_FLAG_ANSWERED)); } static long -SearchKeyHandlerUndeleted(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerUndeleted(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SearchRemainingMatches(session, key, SearchRemainingMatchFlagNot, NULL, (void *)STORE_MSG_FLAG_DELETED)); } static long -SearchKeyHandlerUndraft(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerUndraft(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SearchRemainingMatches(session, key, SearchRemainingMatchFlagNot, NULL, (void *)STORE_MSG_FLAG_DRAFT)); } static long -SearchKeyHandlerUnflagged(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerUnflagged(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SearchRemainingMatches(session, key, SearchRemainingMatchFlagNot, NULL, (void *)STORE_MSG_FLAG_FLAGGED)); } static long -SearchKeyHandlerUnkeyword(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerUnkeyword(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { long ccode; char *utf8SearchString; @@ -970,7 +970,7 @@ SearchKeyHandlerUnkeyword(ImapSession *session, char **keyString, SearchKey *key } static long -SearchKeyHandlerUnseen(ImapSession *session, char **keyString, SearchKey *key) +SearchKeyHandlerUnseen(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { return(SearchRemainingMatches(session, key, SearchRemainingMatchFlagNot, NULL, (void *)STORE_MSG_FLAG_SEEN)); } @@ -1017,7 +1017,7 @@ static SearchKeyInfo SearchKeyList[] = { }; long -SearchHandleKey(ImapSession *session, char **keyString, SearchKey *key) +SearchHandleKey(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { long keyId; @@ -1034,7 +1034,7 @@ SearchHandleKey(ImapSession *session, char **keyString, SearchKey *key) } static long -SearchHandleSubKey(ImapSession *session, char **keyString, SearchKey *key) +SearchHandleSubKey(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { long ccode; @@ -1058,7 +1058,7 @@ SearchHandleSubKey(ImapSession *session, char **keyString, SearchKey *key) static long -SearchHandleTopKey(ImapSession *session, char **keyString, SearchKey *key) +SearchHandleTopKey(ImapSession *session __attribute__((unused)), char **keyString __attribute__((unused)), SearchKey *key __attribute__((unused))) { long ccode; @@ -1080,7 +1080,7 @@ SearchHandleTopKey(ImapSession *session, char **keyString, SearchKey *key) } __inline static long -SearchSendResults(ImapSession *session, long *matches, BOOL byUid) +SearchSendResults(ImapSession *session __attribute__((unused)), long *matches, BOOL byUid) { MessageInformation *message = NULL; if (byUid) { @@ -1120,7 +1120,7 @@ SearchSendResults(ImapSession *session, long *matches, BOOL byUid) static long -SearchHandleCommand(ImapSession *session, char *parameters, BOOL byUid) +SearchHandleCommand(ImapSession *session __attribute__((unused)), char *parameters, BOOL byUid) { long ccode; char *ptr = parameters; @@ -1165,7 +1165,7 @@ ImapCommandSearch(void *param) { long ccode; - ImapSession *session = (ImapSession *)param; + ImapSession *session __attribute__((unused)) = (ImapSession *)param; if ((ccode = SearchHandleCommand(session, session->command.buffer + strlen("SEARCH "), FALSE)) == STATUS_CONTINUE) { return(SendOk(session, "SEARCH")); @@ -1179,7 +1179,7 @@ ImapCommandUidSearch(void *param) { long ccode; - ImapSession *session = (ImapSession *)param; + ImapSession *session __attribute__((unused)) = (ImapSession *)param; if ((ccode = SearchHandleCommand(session, session->command.buffer + strlen("UID SEARCH "), TRUE)) == STATUS_CONTINUE) { return(SendOk(session, "UID SEARCH")); diff --git a/src/agents/imap/session.c b/src/agents/imap/session.c index 9a76e68..717d4ce 100644 --- a/src/agents/imap/session.c +++ b/src/agents/imap/session.c @@ -26,13 +26,15 @@ #include #include +#include #include "imapd.h" static long -UserAuthenticate(ImapSession *session, unsigned char **userName, unsigned char *password, struct sockaddr_in *storeAddress) +UserAuthenticate(ImapSession *session, char **userName, char *password, struct sockaddr_in *storeAddress) { + UNUSED_PARAMETER(session) if (MsgAuthFindUser(*userName) != 0) return(STATUS_USERNAME_NOT_FOUND); @@ -43,10 +45,29 @@ UserAuthenticate(ImapSession *session, unsigned char **userName, unsigned char * return(STATUS_CONTINUE); } +static int +ImapSaslVerify(void *context, const char *user, const char *password, + size_t passwordLength) +{ + char *copy; + int valid; + UNUSED_PARAMETER(context) + if (!user || !password || !passwordLength || strlen(user) > MAXEMAILNAMESIZE) + return 0; + copy = MemMalloc(passwordLength + 1); + if (!copy) return 0; + memcpy(copy, password, passwordLength); + copy[passwordLength] = '\0'; + valid = MsgAuthFindUser(user) == 0 && MsgAuthVerifyPassword(user, copy) == 0; + memset(copy, 0, passwordLength); + MemFree(copy); + return valid; +} + __inline static long UserStoreConnect(ImapSession *session, struct sockaddr_in *store) { - unsigned char buffer[CONN_BUFSIZE + 1]; + char buffer[CONN_BUFSIZE + 1]; if ((session->store.conn = NMAPConnectEx(NULL, store, session->client.conn->trace.destination)) != NULL) { if (NMAPAuthenticateToStore(session->store.conn, buffer, sizeof(buffer))) { @@ -67,7 +88,7 @@ UserStoreConnect(ImapSession *session, struct sockaddr_in *store) } __inline static long -UserStoreInitialize(ImapSession *session, unsigned char *username) +UserStoreInitialize(ImapSession *session, char *username) { long ccode; @@ -99,7 +120,7 @@ UserStoreInitialize(ImapSession *session, unsigned char *username) } __inline static long -UserLogin(ImapSession *session, unsigned char **username, unsigned char *password) +UserLogin(ImapSession *session, char **username, char *password) { long ccode; struct sockaddr_in store; @@ -121,9 +142,9 @@ UserLogin(ImapSession *session, unsigned char **username, unsigned char *passwor /* in X500 spaces and underscores are treated the same in distinguished names */ __inline static void -X500Encode(unsigned char *in) +X500Encode(char *in) { - unsigned char *spacePtr; + char *spacePtr; do { spacePtr = strchr(in, ' '); @@ -137,7 +158,7 @@ X500Encode(unsigned char *in) __inline static long -SendLoginFailure(ImapSession *session, unsigned char *username, unsigned char *command, long result) +SendLoginFailure(ImapSession *session, char *username, char *command, long result) { if ((result == STATUS_WRONG_PASSWORD) || (result == STATUS_USERNAME_NOT_FOUND)) { XplDelay(2000); @@ -158,7 +179,7 @@ SendLoginFailure(ImapSession *session, unsigned char *username, unsigned char *c __inline static long -GetLoginCredentials(ImapSession *session, unsigned char **username, unsigned char **password) +GetLoginCredentials(ImapSession *session, char **username, char **password) { long ccode; @@ -167,13 +188,13 @@ GetLoginCredentials(ImapSession *session, unsigned char **username, unsigned cha if (ccode == STATUS_CONTINUE) { *username = MemStrdup(session->command.buffer); if (*username) { - DecodeBase64(*username); + DecodeBase64((char *)*username); if ((ConnWrite(session->client.conn, "+ UGFzc3dvcmQ6AA==\r\n", 20) != -1) && (ConnFlush(session->client.conn) != -1)) { ccode = ReadCommandLine(session->client.conn, &(session->command.buffer), &(session->command.bufferLen)); if (ccode == STATUS_CONTINUE) { *password = MemStrdup(session->command.buffer); if (*password) { - DecodeBase64(*password); + DecodeBase64((char *)*password); return(STATUS_CONTINUE); } ccode = STATUS_MEMORY_ERROR; @@ -197,38 +218,83 @@ int ImapCommandAuthenticate(void *param) { ImapSession *session = (ImapSession *)param; - long ccode; - unsigned char *username = NULL; - unsigned char *password = NULL; + BongoSaslSession *sasl = NULL; + const char *saslOutput = NULL, *saslUser; + char *mechanism, *initial = NULL, *space, *username = NULL; + char decoded[CONN_BUFSIZE + 1], encoded[CONN_BUFSIZE * 2]; + size_t decodedLength = 0, encodedLength = 0, saslOutputLength = 0; + struct sockaddr_in store; + long ccode = STATUS_ABORT; + int saslResult; - if (session->client.state == STATE_FRESH) { - if (XplStrNCaseCmp(session->command.buffer + 13, "LOGIN", 5) == 0) { - ccode = GetLoginCredentials(session, &username, &password); - if (ccode == STATUS_CONTINUE) { - X500Encode(username); - ccode = UserLogin(session, &username, password); - MemFree(password); - if (ccode == STATUS_CONTINUE) { - session->user.name = username; - session->client.state = STATE_AUTH; - ccode = SendOk(session,"AUTHENTICATE"); - if (ccode == STATUS_CONTINUE) { - /* We are flushing here so that we can update the connection manager */ - /* while the client is working on the response and sending the next command */ - if (ConnFlush(session->client.conn) != -1) { - return(STATUS_CONTINUE); - } - } - } - ccode = SendLoginFailure(session, username, "AUTHENTICATE", ccode); - MemFree(username); - return(ccode); - } - return(SendError(session->client.conn, session->command.tag, "AUTHENTICATE", ccode)); - } - return(SendError(session->client.conn, session->command.tag, "AUTHENTICATE", STATUS_UNKNOWN_AUTH)); + if (session->client.state != STATE_FRESH || !session->client.conn->ssl.enable) + return SendError(session->client.conn, session->command.tag, + "AUTHENTICATE", STATUS_INVALID_STATE); + mechanism = session->command.buffer + strlen(IMAP_COMMAND_AUTHENTICATE); + while (*mechanism == ' ') mechanism++; + if (!*mechanism) return SendError(session->client.conn, session->command.tag, + "AUTHENTICATE", STATUS_INVALID_ARGUMENT); + space = strchr(mechanism, ' '); + if (space) { + *space++ = '\0'; + while (*space == ' ') space++; + if (*space) initial = space; } - return(SendError(session->client.conn, session->command.tag, "AUTHENTICATE", STATUS_INVALID_STATE)); + if (!BongoSaslSessionCreate(&sasl, "imap", BongoGlobals.hostname, + NULL, NULL, 1, ImapSaslVerify, session)) + return SendError(session->client.conn, session->command.tag, + "AUTHENTICATE", STATUS_NMAP_COMM_ERROR); + if (initial && strcmp(initial, "=")) { + if (!BongoSaslDecode64(initial, strlen(initial), decoded, sizeof(decoded), + &decodedLength)) goto authentication_failed; + } + saslResult = BongoSaslStart(sasl, mechanism, initial ? decoded : NULL, + decodedLength, (const void **)&saslOutput, + &saslOutputLength); + while (saslResult == BONGO_SASL_CONTINUE) { + if (saslOutputLength && + !BongoSaslEncode64(saslOutput, saslOutputLength, encoded, + sizeof(encoded), &encodedLength)) goto finish; + if (ConnWriteF(session->client.conn, "+ %.*s\r\n", (int)encodedLength, + saslOutputLength ? encoded : "") == -1 || + ConnFlush(session->client.conn) == -1 || + ReadCommandLine(session->client.conn, &session->command.buffer, + &session->command.bufferLen) != STATUS_CONTINUE) goto finish; + if (!strcmp(session->command.buffer, "*")) { + ccode = SendError(session->client.conn, session->command.tag, + "AUTHENTICATE", STATUS_ABORT); + goto finish; + } + if (!BongoSaslDecode64(session->command.buffer, session->command.bufferLen, + decoded, sizeof(decoded), &decodedLength)) + goto authentication_failed; + saslResult = BongoSaslStep(sasl, decoded, decodedLength, + (const void **)&saslOutput, &saslOutputLength); + } + if (saslResult != BONGO_SASL_COMPLETE || + !(saslUser = BongoSaslUsername(sasl)) || !(username = MemStrdup(saslUser))) + goto authentication_failed; + X500Encode(username); + MsgAuthGetUserStore(username, &store); + ccode = UserStoreConnect(session, &store); + if (ccode == STATUS_CONTINUE) ccode = UserStoreInitialize(session, username); + if (ccode == STATUS_CONTINUE) { + Log(LOG_INFO, "Successful SASL login for user %s", username); + session->user.name = username; + username = NULL; + session->client.state = STATE_AUTH; + ccode = SendOk(session, "AUTHENTICATE"); + if (ccode == STATUS_CONTINUE && ConnFlush(session->client.conn) == -1) + ccode = STATUS_ABORT; + goto finish; + } +authentication_failed: + ccode = SendLoginFailure(session, username ? username : "", "AUTHENTICATE", + STATUS_WRONG_PASSWORD); +finish: + MemFree(username); + BongoSaslSessionDestroy(&sasl); + return ccode; } int @@ -236,10 +302,10 @@ ImapCommandLogin(void *param) { ImapSession *session = (ImapSession *)param; long ccode; - unsigned char *username = NULL; - unsigned char *password = NULL; + char *username = NULL; + char *password = NULL; - if (session->client.state == STATE_FRESH) { + if (session->client.state == STATE_FRESH && session->client.conn->ssl.enable) { ccode = GrabTwoArguments(session, session->command.buffer + 6, &username, &password); if (ccode == STATUS_CONTINUE) { X500Encode(username); diff --git a/src/agents/imap/store.c b/src/agents/imap/store.c index e438d02..18d4186 100644 --- a/src/agents/imap/store.c +++ b/src/agents/imap/store.c @@ -44,9 +44,9 @@ static StoreCommandType StoreCommandTypes[] = { }; unsigned long -ParseStoreFlags(unsigned char *command, unsigned long *returnFlags) +ParseStoreFlags(char *command, unsigned long *returnFlags) { - unsigned char *ptr; + char *ptr; unsigned long flags; long ccode; @@ -172,12 +172,12 @@ DoStoreForMessageSet(ImapSession *session, char *messageSet, unsigned long Flags __inline static long HandleStore(ImapSession *session, BOOL ByUID, BOOL *purgedMessage) { - unsigned char *messageSet; + char *messageSet; long ccode; long keywordId; - unsigned char *ptr; - unsigned char *type; - unsigned char *flagString; + char *ptr; + char *type; + char *flagString; unsigned long flags; if ((ccode = CheckState(session, STATE_SELECTED)) == STATUS_CONTINUE) { diff --git a/src/agents/imap/store.h b/src/agents/imap/store.h index f01f861..66171e6 100644 --- a/src/agents/imap/store.h +++ b/src/agents/imap/store.h @@ -21,17 +21,16 @@ typedef struct { - unsigned char *string; + const char *string; unsigned long stringLen; unsigned long value; } StoreCommandFlag; typedef struct { - unsigned char *string; - unsigned char *nmapCommand; + const char *string; + char *nmapCommand; BOOL silent; } StoreCommandType; -unsigned long ParseStoreFlags(unsigned char *command, unsigned long *returnFlags); - +unsigned long ParseStoreFlags(char *command, unsigned long *returnFlags); diff --git a/src/agents/imap/uid.c b/src/agents/imap/uid.c index e10f034..3d1f2d8 100644 --- a/src/agents/imap/uid.c +++ b/src/agents/imap/uid.c @@ -88,7 +88,7 @@ UidToSequenceNum(MessageInformation *message, unsigned long messageCount, unsign __inline static long -UidToSequenceRange(MessageInformation *message, unsigned long messageCount, unsigned long requestUidStart, unsigned long requestUidEnd, long *startNum, long *endNum) +UidToSequenceRange(MessageInformation *message, unsigned long messageCount, unsigned long requestUidStart, unsigned long requestUidEnd, unsigned long *startNum, unsigned long *endNum) { if (messageCount != 0) { unsigned long lastMessageId; @@ -289,5 +289,14 @@ GetMessageRange(MessageInformation *message, unsigned long messageCount, char ** long TestUidToSequenceRange(MessageInformation *message, unsigned long messageCount, unsigned long requestUidStart, unsigned long requestUidEnd, long *startNum, long *endNum) { - return(UidToSequenceRange(message, messageCount, requestUidStart, requestUidEnd, startNum, endNum)); + unsigned long unsignedStart; + unsigned long unsignedEnd; + long result = UidToSequenceRange(message, messageCount, requestUidStart, + requestUidEnd, &unsignedStart, &unsignedEnd); + + if (result == STATUS_CONTINUE) { + *startNum = (long)unsignedStart; + *endNum = (long)unsignedEnd; + } + return result; } diff --git a/src/agents/pop/CMakeLists.txt b/src/agents/pop/CMakeLists.txt index 210c5dd..b24f100 100644 --- a/src/agents/pop/CMakeLists.txt +++ b/src/agents/pop/CMakeLists.txt @@ -8,6 +8,7 @@ target_link_libraries(bongopop3 bongoutil bongojson bongomsgapi + bongosasl ) install(TARGETS bongopop3 DESTINATION ${SBIN_INSTALL_DIR}) diff --git a/src/agents/pop/pop3.c b/src/agents/pop/pop3.c index a4073e1..645bfcc 100644 --- a/src/agents/pop/pop3.c +++ b/src/agents/pop/pop3.c @@ -36,6 +36,7 @@ #include #include #include +#include #include @@ -98,7 +99,7 @@ static void SignalHandler(int sigtype); } else if (XplSafeRead(POP3.client.worker.active) < XplSafeRead(POP3.client.worker.maximum)) { \ XplSafeIncrement(POP3.client.worker.active); \ XplSignalBlock(); \ - XplBeginThread(&(id), HandleConnection, POP_STACK_SPACE, (char *)(XplSafeRead(POP3.client.worker.active)), (r)); \ + XplBeginThread(&(id), HandleConnection, POP_STACK_SPACE, (void *)(intptr_t)XplSafeRead(POP3.client.worker.active), (r)); \ XplSignalHandler(SignalHandler); \ if (!(r)) { \ (c)->queue.previous = NULL; \ @@ -166,10 +167,10 @@ typedef struct _POP3Client { ProtocolCommand *handler; - unsigned char user[MAXEMAILNAMESIZE + 1]; + char user[MAXEMAILNAMESIZE + 1]; - unsigned char command[CONN_BUFSIZE + 1]; - unsigned char buffer[CONN_BUFSIZE + 1]; + char command[CONN_BUFSIZE + 1]; + char buffer[CONN_BUFSIZE + 1]; unsigned long messageCount; unsigned long messagesDeleted; @@ -182,6 +183,11 @@ typedef struct _POP3Client { int loginCount; } POP3Client; +static int POP3OpenMailbox(POP3Client *client); +static int POP3SaslVerify(void *context, const char *user, + const char *password, size_t password_length); +static BOOL LoadIDList(POP3Client *client); + struct { enum POP3States state; @@ -200,6 +206,7 @@ struct { struct { struct { BOOL enable; + BOOL allowLegacyTLS; ConnSSLConfiguration config; @@ -245,8 +252,8 @@ struct { Connection *conn; } ssl; - unsigned char address[80]; - unsigned char hash[NMAP_HASH_SIZE]; + char address[80]; + char hash[NMAP_HASH_SIZE]; } nmap; struct { @@ -257,7 +264,7 @@ struct { XplAtomic badPasswords; } stats; - unsigned char managementURL[256]; + char managementURL[256]; void *loggingHandle; @@ -285,29 +292,31 @@ static int POP3CommandUser(void *param); static int POP3CommandUIDL(void *param); static int POP3CommandXTND(void *param); +#define POP_COMMAND(name, callback) \ + { name, NULL, sizeof(name) - 1, callback, NULL, NULL, NULL, 0 } static ProtocolCommand POP3CommandEntries[] = { - { POP3_COMMAND_APOP, NULL, sizeof(POP3_COMMAND_APOP) - 1, POP3CommandAPOP, NULL, NULL }, - { POP3_COMMAND_AUTH, NULL, sizeof(POP3_COMMAND_AUTH) - 1, POP3CommandAuth, NULL, NULL }, - { POP3_COMMAND_CAPA, NULL, sizeof(POP3_COMMAND_CAPA) - 1, POP3CommandCapa, NULL, NULL }, - { POP3_COMMAND_DELE, NULL, sizeof(POP3_COMMAND_DELE) - 1, POP3CommandDele, NULL, NULL }, - { POP3_COMMAND_GURL, NULL, sizeof(POP3_COMMAND_GURL) - 1, POP3CommandGURL, NULL, NULL }, - { POP3_COMMAND_LAST, NULL, sizeof(POP3_COMMAND_LAST) - 1, POP3CommandLast, NULL, NULL }, - { POP3_COMMAND_LIST, NULL, sizeof(POP3_COMMAND_LIST) - 1, POP3CommandList, NULL, NULL }, - { POP3_COMMAND_NOOP, NULL, sizeof(POP3_COMMAND_NOOP) - 1, POP3CommandNoop, NULL, NULL }, - { POP3_COMMAND_PASS, NULL, sizeof(POP3_COMMAND_PASS) - 1, POP3CommandPass, NULL, NULL }, - { POP3_COMMAND_QUIT, NULL, sizeof(POP3_COMMAND_QUIT) - 1, POP3CommandQuit, NULL, NULL }, - { POP3_COMMAND_RETR, NULL, sizeof(POP3_COMMAND_RETR) - 1, POP3CommandRetr, NULL, NULL }, - { POP3_COMMAND_RSET, NULL, sizeof(POP3_COMMAND_RSET) - 1, POP3CommandRSet, NULL, NULL }, - { POP3_COMMAND_STARTTLS, NULL, sizeof(POP3_COMMAND_STARTTLS) - 1, POP3CommandSTLS, NULL, NULL }, - { POP3_COMMAND_STAT, NULL, sizeof(POP3_COMMAND_STAT) - 1, POP3CommandStat, NULL, NULL }, - { POP3_COMMAND_STLS, NULL, sizeof(POP3_COMMAND_STLS) - 1, POP3CommandSTLS, NULL, NULL }, - { POP3_COMMAND_TOP, NULL, sizeof(POP3_COMMAND_TOP) - 1, POP3CommandTop, NULL, NULL }, - { POP3_COMMAND_USER, NULL, sizeof(POP3_COMMAND_USER) - 1, POP3CommandUser, NULL, NULL }, - { POP3_COMMAND_UIDL, NULL, sizeof(POP3_COMMAND_UIDL) - 1, POP3CommandUIDL, NULL, NULL }, - { POP3_COMMAND_XTND, NULL, sizeof(POP3_COMMAND_XTND) - 1, POP3CommandXTND, NULL, NULL }, - - { NULL, NULL, 0, NULL, NULL, NULL } + POP_COMMAND(POP3_COMMAND_APOP, POP3CommandAPOP), + POP_COMMAND(POP3_COMMAND_AUTH, POP3CommandAuth), + POP_COMMAND(POP3_COMMAND_CAPA, POP3CommandCapa), + POP_COMMAND(POP3_COMMAND_DELE, POP3CommandDele), + POP_COMMAND(POP3_COMMAND_GURL, POP3CommandGURL), + POP_COMMAND(POP3_COMMAND_LAST, POP3CommandLast), + POP_COMMAND(POP3_COMMAND_LIST, POP3CommandList), + POP_COMMAND(POP3_COMMAND_NOOP, POP3CommandNoop), + POP_COMMAND(POP3_COMMAND_PASS, POP3CommandPass), + POP_COMMAND(POP3_COMMAND_QUIT, POP3CommandQuit), + POP_COMMAND(POP3_COMMAND_RETR, POP3CommandRetr), + POP_COMMAND(POP3_COMMAND_RSET, POP3CommandRSet), + POP_COMMAND(POP3_COMMAND_STARTTLS, POP3CommandSTLS), + POP_COMMAND(POP3_COMMAND_STAT, POP3CommandStat), + POP_COMMAND(POP3_COMMAND_STLS, POP3CommandSTLS), + POP_COMMAND(POP3_COMMAND_TOP, POP3CommandTop), + POP_COMMAND(POP3_COMMAND_USER, POP3CommandUser), + POP_COMMAND(POP3_COMMAND_UIDL, POP3CommandUIDL), + POP_COMMAND(POP3_COMMAND_XTND, POP3CommandXTND), + { NULL, NULL, 0, NULL, NULL, NULL, NULL, 0 } }; +#undef POP_COMMAND unsigned long POP3ServerPort = POP3_PORT; unsigned long POP3ServerPortSSL = POP3_PORT_SSL; @@ -427,6 +436,7 @@ static BOOL POP3ClientAllocCB(void *buffer, void *clientData) { POP3Client *c = (POP3Client *)buffer; + UNUSED_PARAMETER(clientData) memset(c, 0, sizeof(POP3Client)); @@ -450,7 +460,7 @@ POP3ReturnClientPoolEntry(POP3Client *client) } static int -ConnectUserToNMAPServer(POP3Client *client, unsigned char *username, unsigned char *password) +ConnectUserToNMAPServer(POP3Client *client, char *username, char *password) { BOOL result; struct sockaddr_in nmap; @@ -487,6 +497,55 @@ ConnectUserToNMAPServer(POP3Client *client, unsigned char *username, unsigned ch return(POP3_NMAP_SERVER_DOWN); } +static int +POP3SaslVerify(void *context, const char *user, const char *password, + size_t password_length) +{ + POP3Client *client = context; + char *copy; + int valid; + if (!client || !user || !password || !password_length || + strlen(user) >= sizeof(client->user)) return 0; + copy = MemMalloc(password_length + 1); + if (!copy) return 0; + memcpy(copy, password, password_length); + copy[password_length] = '\0'; + valid = MsgAuthFindUser(user) == 0 && MsgAuthVerifyPassword(user, copy) == 0; + memset(copy, 0, password_length); + MemFree(copy); + return valid; +} + +static int +POP3OpenMailbox(POP3Client *client) +{ + BOOL authenticated; + if (!client->store) { + client->store = NMAPConnectEx("127.0.0.1", NULL, + client->conn->trace.destination); + if (!client->store) return POP3_NMAP_SERVER_DOWN; + authenticated = NMAPAuthenticateToStore(client->store, (char *)client->buffer, + CONN_BUFSIZE); + if (!authenticated) { + NMAPQuit(client->store); + ConnFree(client->store); + client->store = NULL; + return POP3_NMAP_SERVER_DOWN; + } + } + if (NMAPSendCommandF(client->store, "USER %s\r\n", client->user) == -1 || + NMAPReadAnswer(client->store, (char *)client->buffer, CONN_BUFSIZE, TRUE) != NMAP_OK || + NMAPSendCommandF(client->store, "STORE %s\r\n", client->user) == -1 || + NMAPReadAnswer(client->store, (char *)client->buffer, CONN_BUFSIZE, TRUE) != NMAP_OK || + !LoadIDList(client)) { + NMAPQuit(client->store); + ConnFree(client->store); + client->store = NULL; + return POP3_NMAP_SERVER_DOWN; + } + return 0; +} + static BOOL LoadIDList(POP3Client *client) { @@ -540,7 +599,7 @@ LoadIDList(POP3Client *client) } } - result = sscanf(client->buffer, "%*u %llx %u %u %*x %*u %lu %*s", &guid, &type, &flags, &size); + result = sscanf(client->buffer, "%*u %" SCNx64 " %u %u %*x %*u %lu %*s", &guid, &type, &flags, &size); if(result != 4) { /* sscanf failed - must have been passed a bad LIST string - bail out! */ ccode = -1; @@ -598,8 +657,111 @@ static int POP3CommandAuth(void *param) { POP3Client *client = (POP3Client *)param; + BongoSaslSession *session = NULL; + const char *mechanisms = NULL, *username, *output = NULL; + char *mechanism, *initial = NULL, *space; + char decoded[CONN_BUFSIZE + 1]; + char encoded[CONN_BUFSIZE * 2]; + size_t decodedLength = 0, encodedLength = 0, outputLength = 0; + int result = BONGO_SASL_FAIL, ccode = -1; - return(ConnWrite(client->conn, MSGERRNOTSUPPORTED, sizeof(MSGERRNOTSUPPORTED) - 1)); + if (client->state != POP3_CLIENT_FRESH) + return ConnWrite(client->conn, MSGERRBADSTATE, sizeof(MSGERRBADSTATE) - 1); + /* Password-bearing mechanisms are deliberately unavailable before TLS. */ + if (!client->conn->ssl.enable) + return ConnWrite(client->conn, "-ERR [AUTH] Encryption required\r\n", + sizeof("-ERR [AUTH] Encryption required\r\n") - 1); + if (!BongoSaslSessionCreate(&session, "pop", BongoGlobals.hostname, + NULL, NULL, 1, POP3SaslVerify, client)) + return ConnWrite(client->conn, "-ERR [SYS/TEMP] Authentication unavailable\r\n", + sizeof("-ERR [SYS/TEMP] Authentication unavailable\r\n") - 1); + if (client->command[4] == '\0') { + char *list, *token, *save = NULL; + if (!BongoSaslMechanisms(session, &mechanisms) || + !(list = MemStrdup(mechanisms))) goto finish; + ccode = ConnWrite(client->conn, "+OK Supported SASL mechanisms\r\n", + sizeof("+OK Supported SASL mechanisms\r\n") - 1); + for (token = strtok_r(list, " ", &save); ccode != -1 && token; + token = strtok_r(NULL, " ", &save)) + ccode = ConnWriteF(client->conn, "%s\r\n", token); + MemFree(list); + if (ccode != -1) ccode = ConnWrite(client->conn, ".\r\n", 3); + goto finish; + } + if (client->command[4] != ' ') { + ccode = ConnWrite(client->conn, MSGERRSYNTAX, sizeof(MSGERRSYNTAX) - 1); + goto finish; + } + mechanism = client->command + 5; + while (*mechanism == ' ') mechanism++; + space = strchr(mechanism, ' '); + if (space) { + *space++ = '\0'; + while (*space == ' ') space++; + initial = space; + } + if (!*mechanism || strchr((char *)mechanism, '\r') || strchr((char *)mechanism, '\n')) { + ccode = ConnWrite(client->conn, MSGERRSYNTAX, sizeof(MSGERRSYNTAX) - 1); + goto finish; + } + if (initial && strcmp(initial, "=")) { + if (!BongoSaslDecode64(initial, strlen(initial), decoded, + sizeof(decoded), &decodedLength)) goto authentication_failed; + } + result = BongoSaslStart(session, mechanism, + initial ? decoded : NULL, decodedLength, + (const void **)&output, &outputLength); + while (result == BONGO_SASL_CONTINUE) { + if (outputLength && !BongoSaslEncode64(output, outputLength, encoded, + sizeof(encoded), &encodedLength)) + goto authentication_failed; + if (ConnWriteF(client->conn, "+ %.*s\r\n", (int)encodedLength, + outputLength ? encoded : "") == -1 || + ConnFlush(client->conn) == -1 || + ConnReadAnswer(client->conn, client->buffer, CONN_BUFSIZE) == -1) goto finish; + if (!strcmp(client->buffer, "*")) { + ccode = ConnWrite(client->conn, "-ERR [AUTH] Authentication cancelled\r\n", + sizeof("-ERR [AUTH] Authentication cancelled\r\n") - 1); + goto finish; + } + if (!BongoSaslDecode64(client->buffer, strlen(client->buffer), + decoded, sizeof(decoded), &decodedLength)) + goto authentication_failed; + result = BongoSaslStep(session, decoded, decodedLength, + (const void **)&output, &outputLength); + } + if (result != BONGO_SASL_COMPLETE || !(username = BongoSaslUsername(session)) || + strlen(username) >= sizeof(client->user)) goto authentication_failed; + strcpy(client->user, username); + if (!POP3OpenMailbox(client)) { + client->state = POP3_CLIENT_TRANSACTION; + Log(LOG_INFO, "User %s logged in with SASL from host %s", client->user, + LOGIP(client->conn->socketAddress)); + ccode = ConnWrite(client->conn, MSGOK, sizeof(MSGOK) - 1); + goto finish; + } + ccode = ConnWrite(client->conn, "-ERR [SYS/TEMP] Mailbox unavailable\r\n", + sizeof("-ERR [SYS/TEMP] Mailbox unavailable\r\n") - 1); + goto finish; + +authentication_failed: + XplSafeIncrement(POP3.stats.badPasswords); + client->loginCount++; + XplDelay(2000); + if (client->loginCount >= MAX_LOGIN_FAILURES) { + ccode = ConnWrite(client->conn, MSGERRNOPERMISSIONCLOSING, + sizeof(MSGERRNOPERMISSIONCLOSING) - 1); + ConnFlush(client->conn); + ConnClose(client->conn); + ConnFree(client->conn); + client->conn = NULL; + } else { + ccode = ConnWrite(client->conn, MSGERRNOPERMISSION, + sizeof(MSGERRNOPERMISSION) - 1); + } +finish: + BongoSaslSessionDestroy(&session); + return ccode; } #if 0 @@ -633,7 +795,7 @@ POP3CommandAuth(void *param) if (ccode == -1) break; /* fixme - using static salts? */ - DecodeBase64(client->buffer); + DecodeBase64((char *)client->buffer); ccode = ConnWrite(client->conn, "+ UGFzc3dvcmQ6AA==\r\n", 20); if (ccode == -1) break; @@ -644,7 +806,7 @@ POP3CommandAuth(void *param) ccode = ConnReadAnswer(client->conn, client->command, CONN_BUFSIZE); if (ccode == -1) break; - DecodeBase64(client->command); + DecodeBase64((char *)client->command); strcpy(client->user, client->buffer); @@ -744,12 +906,22 @@ POP3CommandCapa(void *param) { int ccode; POP3Client *client = (POP3Client *)param; + BongoSaslSession *session = NULL; + const char *mechanisms = NULL; - if (POP3.server.ssl.enable) { - ccode = ConnWrite(client->conn, MSGCAPALIST, sizeof(MSGCAPALIST) - 1); - } else { - ccode = ConnWrite(client->conn, MSGCAPALIST_NOTLS, sizeof(MSGCAPALIST_NOTLS) - 1); - } + ccode = ConnWrite(client->conn, "+OK\r\nTOP\r\nRESP-CODES\r\nAUTH-RESP-CODE\r\n" + "PIPELINING\r\nEXPIRE NEVER\r\nUIDL\r\n", + sizeof("+OK\r\nTOP\r\nRESP-CODES\r\nAUTH-RESP-CODE\r\n" + "PIPELINING\r\nEXPIRE NEVER\r\nUIDL\r\n") - 1); + if (ccode != -1 && client->conn->ssl.enable && + BongoSaslSessionCreate(&session, "pop", BongoGlobals.hostname, + NULL, NULL, 1, POP3SaslVerify, client) && + BongoSaslMechanisms(session, &mechanisms)) + ccode = ConnWriteF(client->conn, "SASL %s\r\nUSER\r\n", mechanisms); + if (ccode != -1 && !client->conn->ssl.enable) + ccode = ConnWrite(client->conn, "STLS\r\n", 6); + if (ccode != -1) ccode = ConnWrite(client->conn, ".\r\n", 3); + BongoSaslSessionDestroy(&session); return(ccode); } @@ -891,7 +1063,7 @@ static int POP3CommandPass(void *param) { int ccode; - unsigned char *ptr; + char *ptr; POP3Client *client = (POP3Client *)param; if (client->state == POP3_CLIENT_AUTHORIZATION) { @@ -933,69 +1105,8 @@ POP3CommandPass(void *param) return(-1); } - do - { - ccode = NMAPSendCommandF(client->store, "USER %s\r\n", client->user); - if (ccode == -1) { - ConnWriteF(client->conn, "%s %d\r\n", MSGERRNMAP, ccode); - break; - } - - ccode = NMAPReadAnswer(client->store, client->buffer, CONN_BUFSIZE, TRUE); - switch(ccode) { - case 3010: - case 3241: { - ConnWrite(client->conn, MSGERRNOBOX, sizeof(MSGERRNOBOX) - 1); - break; - } - - case 1000: { - ccode = 0; - break; - } - - default: { - ConnWriteF(client->conn, "-ERR [SYS/PERM] %s %s\r\n", BongoGlobals.hostname, MSGERRNONMAP); - break; - } - } - - if (ccode) break; - - ccode = NMAPSendCommandF(client->store, "STORE %s\r\n", client->user); - if (ccode == -1) { - ccode = ConnWriteF(client->conn, "%s %d\r\n", MSGERRNMAP, ccode); - break; - } - - ccode = NMAPReadAnswer(client->store, client->buffer, CONN_BUFSIZE, TRUE); - switch(ccode) { - case 3010: - case 3241: { - ConnWrite(client->conn, MSGERRNOBOX, sizeof(MSGERRNOBOX) - 1); - break; - } - - case 1000: { - ccode = 0; - break; - } - - default: { - ConnWriteF(client->conn, "-ERR [SYS/PERM] %s %s\r\n", BongoGlobals.hostname, MSGERRNONMAP); - break; - } - } - - if (ccode) break; - - if (!LoadIDList(client)) { - ccode = -1; - ConnWriteF(client->conn, "%s %d\r\n", MSGERRNMAP, ccode); - break; - } - ccode = 0; - } while (0); + ccode = POP3OpenMailbox(client); + if (ccode) ConnWriteF(client->conn, "%s %d\r\n", MSGERRNMAP, ccode); if (ccode == 0) { client->state = POP3_CLIENT_TRANSACTION; @@ -1025,7 +1136,7 @@ POP3CommandQuit(void *param) if(client->message[id].deleted) { if (ccode != -1) { - ccode = NMAPSendCommandF(client->store, "DELETE %llx\r\n", client->message[id].guid); + ccode = NMAPSendCommandF(client->store, "DELETE %" PRIx64 "\r\n", client->message[id].guid); } if (ccode != -1) { @@ -1037,6 +1148,7 @@ POP3CommandQuit(void *param) lastccode = ccode; } } + break; } } } @@ -1062,7 +1174,7 @@ POP3CommandRetr(void *param) long length; long tmp; unsigned long id; - unsigned char *ptr; + char *ptr; POP3Client *client = (POP3Client *)param; if (client->state == POP3_CLIENT_TRANSACTION) { @@ -1079,7 +1191,7 @@ POP3CommandRetr(void *param) return(ConnWrite(client->conn, MSGERRNOMSG, sizeof(MSGERRNOMSG) - 1)); } - ccode = NMAPSendCommandF(client->store, "READ %llx\r\n", client->message[id].guid); + ccode = NMAPSendCommandF(client->store, "READ %" PRIx64 "\r\n", client->message[id].guid); if (ccode != -1) { ccode = NMAPReadAnswer(client->store, client->buffer, CONN_BUFSIZE, TRUE); @@ -1133,7 +1245,7 @@ POP3CommandRetr(void *param) if (ccode != -1) { /* Mark as read */ - ccode = NMAPSendCommandF(client->store, "FLAG %llx +%lu\r\n", client->message[id].guid, (long unsigned int)STORE_MSG_FLAG_SEEN); + ccode = NMAPSendCommandF(client->store, "FLAG %" PRIx64 " +%lu\r\n", client->message[id].guid, (long unsigned int)STORE_MSG_FLAG_SEEN); } if (ccode != -1) { @@ -1142,6 +1254,7 @@ POP3CommandRetr(void *param) break; } + break; } case 4220: { @@ -1210,7 +1323,7 @@ POP3CommandSTLS(void *param) BOOL result; POP3Client *client = (POP3Client *)param; - if (client->state == POP3_CLIENT_FRESH) { + if (client->state == POP3_CLIENT_FRESH && !client->conn->ssl.enable) { if (POP3.server.ssl.enable) { ccode = ConnWrite(client->conn, MSGOKSTARTTLS, sizeof(MSGOKSTARTTLS) - 1); } else { @@ -1248,8 +1361,8 @@ POP3CommandTop(void *param) long length; long lines; unsigned long id; - unsigned long headerSize; - unsigned char *ptr; + long headerSize; + char *ptr; POP3Client *client = (POP3Client *)param; if (client->state == POP3_CLIENT_TRANSACTION) { @@ -1271,11 +1384,11 @@ POP3CommandTop(void *param) /* Read message header size */ ccode = NMAPGetDecimalProperty(client->store, client->message[id].guid, "nmap.mail.headersize", &headerSize); - if (ccode == -1) + if (ccode == -1 || headerSize < 0 || (unsigned long)headerSize > client->message[id].size) return (ConnWriteF(client->conn, "%s %d\r\n", MSGERRNMAP, ccode)); /* Read the message header */ - ccode = NMAPSendCommandF(client->store, "READ %llx 0 %lu\r\n", client->message[id].guid, headerSize); + ccode = NMAPSendCommandF(client->store, "READ %" PRIx64 " 0 %ld\r\n", client->message[id].guid, headerSize); if (ccode != -1) { ccode = NMAPReadPropertyValueLength(client->store, "nmap.document", &count); switch(ccode) { @@ -1306,7 +1419,9 @@ POP3CommandTop(void *param) } if (lines) { - ccode = NMAPSendCommandF(client->store, "READ %llx %lu %lu\r\n", client->message[id].guid, headerSize, (client->message[id].size - headerSize)); + ccode = NMAPSendCommandF(client->store, "READ %" PRIx64 " %ld %lu\r\n", + client->message[id].guid, headerSize, + client->message[id].size - (unsigned long)headerSize); if (ccode != -1) { ccode = NMAPReadPropertyValueLength(client->store, "nmap.document", &count); switch(ccode) { @@ -1382,7 +1497,7 @@ POP3CommandUIDL(void *param) return(ConnWrite(client->conn, MSGERRNOMSG, sizeof(MSGERRNOMSG) - 1)); } - ccode = ConnWriteF(client->conn, "+OK %ld %llx\r\n", id + 1, client->message[id].guid); + ccode = ConnWriteF(client->conn, "+OK %ld %" PRIx64 "\r\n", id + 1, client->message[id].guid); return(ccode); } @@ -1394,7 +1509,7 @@ POP3CommandUIDL(void *param) if(client->message[id].deleted) { continue; } - ccode = ConnWriteF(client->conn, "%lu %llx\r\n", id + 1, client->message[id].guid); + ccode = ConnWriteF(client->conn, "%lu %" PRIx64 "\r\n", id + 1, client->message[id].guid); } ccode = ConnWrite(client->conn, ".\r\n", 3); } @@ -1407,12 +1522,12 @@ static int POP3CommandUser(void *param) { int ccode; - unsigned char *ptr; - unsigned char *dest; - unsigned char *limit; + char *ptr; + char *dest; + char *limit; POP3Client *client = (POP3Client *)param; - if (client->state == POP3_CLIENT_FRESH) { + if (client->state == POP3_CLIENT_FRESH && client->conn->ssl.enable) { ptr = client->command + 5; while (isspace(*ptr)) { ptr++; @@ -1461,7 +1576,7 @@ static BOOL HandleConnection(void *param) { int ccode; - long threadNumber = (long)param; + long threadNumber = (long)(intptr_t)param; BOOL result; time_t last = time(NULL); time_t current; @@ -1678,6 +1793,7 @@ static void POP3Server(void *ignored) { int ccode; + UNUSED_PARAMETER(ignored) XplThreadID id; Connection *conn; @@ -1867,6 +1983,7 @@ static void POP3SSLServer(void *ignored) { int ccode; + UNUSED_PARAMETER(ignored) XplThreadID id; Connection *conn; @@ -1997,6 +2114,7 @@ client->CSSL = NULL; static BongoConfigItem POP3Config[] = { { BONGO_JSON_INT, "o:port/i", &POP3ServerPort }, { BONGO_JSON_INT, "o:port_ssl/i", &POP3ServerPortSSL }, + { BONGO_JSON_BOOL, "o:allow_legacy_tls/b", &POP3.server.ssl.allowLegacyTLS }, { BONGO_JSON_NULL, NULL, NULL } }; @@ -2004,7 +2122,6 @@ static BOOL ReadConfiguration(void) { POP3.server.ssl.config.options |= SSL_ALLOW_CHAIN; - POP3.server.ssl.config.options |= SSL_ALLOW_SSL3; POP3.server.ssl.enable = TRUE; POP3.nmap.ssl.enable = FALSE; // FIXME: why? @@ -2029,6 +2146,8 @@ XplServiceMain(int argc, char *argv[]) { int ccode; XplThreadID id; + UNUSED_PARAMETER(argc) + UNUSED_PARAMETER(argv) if (XplSetEffectiveUser(MsgGetUnprivilegedUser()) < 0) { Log(LOG_ERROR, "Could not drop to unprivileged user '%s'.", MsgGetUnprivilegedUser()); @@ -2054,6 +2173,7 @@ XplServiceMain(int argc, char *argv[]) POP3.server.ssl.enable = FALSE; POP3.server.ssl.context = NULL; POP3.server.ssl.config.options = 0; + POP3.server.ssl.allowLegacyTLS = FALSE; POP3.loggingHandle = NULL; @@ -2086,6 +2206,10 @@ XplServiceMain(int argc, char *argv[]) MsgInit(); MsgAuthInit(); + if (!BongoSaslInitialize()) { + Log(LOG_ERROR, "Unable to initialize Cyrus SASL"); + return -1; + } NMAPInitialize(); LogOpen("bongopop3"); @@ -2103,6 +2227,9 @@ XplServiceMain(int argc, char *argv[]) if (POP3.server.ssl.enable) { if (!ServerSocketSSLInit()) { + if (POP3.server.ssl.allowLegacyTLS) { + POP3.server.ssl.config.options |= SSL_ALLOW_LEGACY_PROTOCOLS; + } POP3.server.ssl.config.certificate.file = MsgGetFile(MSGAPI_FILE_PUBKEY, NULL, 0); POP3.server.ssl.config.key.file = MsgGetFile(MSGAPI_FILE_PRIVKEY, NULL, 0); POP3.server.ssl.config.key.type = GNUTLS_X509_FMT_PEM; diff --git a/src/agents/pop/pop3.h b/src/agents/pop/pop3.h index e6a2f18..b436017 100644 --- a/src/agents/pop/pop3.h +++ b/src/agents/pop/pop3.h @@ -19,6 +19,8 @@ * ****************************************************************************/ +#include + /* Port to listen */ #define POP3_PORT 110 #define POP3_PORT_SSL 995 @@ -35,8 +37,8 @@ #define MSGOK "+OK\r\n" #define MSGOKHELLO "Bongo POP3 server ready\r\n" #define MSGOKBYE "signing off" -#define MSGCAPALIST "+OK\r\nTOP\r\nUSER\r\nSASL LOGIN\r\nRESP-CODES\r\nAUTH-RESP-CODE\r\nPIPELINING\r\nEXPIRE NEVER\r\nUIDL\r\nSTLS\r\n.\r\n" -#define MSGCAPALIST_NOTLS "+OK\r\nTOP\r\nUSER\r\nSASL LOGIN\r\nRESP-CODES\r\nAUTH-RESP-CODE\r\nPIPELINING\r\nEXPIRE NEVER\r\nUIDL\r\n.\r\n" +#define MSGCAPALIST_TLS "+OK\r\nTOP\r\nUSER\r\nRESP-CODES\r\nAUTH-RESP-CODE\r\nPIPELINING\r\nEXPIRE NEVER\r\nUIDL\r\n.\r\n" +#define MSGCAPALIST_STARTTLS "+OK\r\nTOP\r\nRESP-CODES\r\nAUTH-RESP-CODE\r\nPIPELINING\r\nEXPIRE NEVER\r\nUIDL\r\nSTLS\r\n.\r\n" #define MSGWRONGMETHOD "-ERR [AUTH] Unsupported method\r\n" #define MSGOKPASSWD "+OK Password required\r\n" #define MSGOKSTARTTLS "+OK Begin TLS negotiations\r\n" diff --git a/src/agents/queue/conf.c b/src/agents/queue/conf.c index 1c2a8e0..055dc9d 100644 --- a/src/agents/queue/conf.c +++ b/src/agents/queue/conf.c @@ -49,7 +49,7 @@ int aliasCmpFunc (const void *lft, const void *rgt); -QueueConfiguration Conf = { 0, {0, }, }; +QueueConfiguration Conf = {0}; long CalculateCheckQueueLimit(unsigned long concurrent, unsigned long sequential) @@ -205,11 +205,11 @@ ReadConfiguration (BOOL *recover) unsigned int x; for(x=0;xlen;x++) { char *aconfig; - unsigned char path[100]; + char path[100]; BongoJsonNode *node; struct _AliasStruct a; - a.original = MemStrdup(g_array_index(Conf.domains, unsigned char *, x)); + a.original = MemStrdup(g_array_index(Conf.domains, char *, x)); a.to = NULL; a.aliases = NULL; a.mapping_type = 0; @@ -262,6 +262,7 @@ ReadConfiguration (BOOL *recover) void CheckConfig(BongoAgent *agent) { + UNUSED_PARAMETER(agent); /* TODO: Figure out how we are going to handle this type of situation */ return; } diff --git a/src/agents/queue/conf.h b/src/agents/queue/conf.h index ffa0792..78749c7 100644 --- a/src/agents/queue/conf.h +++ b/src/agents/queue/conf.h @@ -108,9 +108,9 @@ typedef struct _QueueConfiguration { } QueueConfiguration; struct _AliasStruct{ - unsigned char* original; - unsigned char* to; - unsigned int mapping_type; + char *original; + char *to; + int mapping_type; GArray *aliases; }; diff --git a/src/agents/queue/domain.c b/src/agents/queue/domain.c index 6b64b1d..32d56d5 100644 --- a/src/agents/queue/domain.c +++ b/src/agents/queue/domain.c @@ -26,7 +26,7 @@ #include "domain.h" int -QDBStartup(int minimum, int maximum) +QDBStartup(int minimum __attribute__((unused)), int maximum __attribute__((unused))) { return 0; } @@ -43,48 +43,48 @@ QDBHandleAlloc(void) } void -QDBHandleRelease(void *handle) +QDBHandleRelease(void *handle __attribute__((unused))) { } int -QDBAdd(void *handle, unsigned char *domain, unsigned long queueID, int queue) +QDBAdd(void *handle __attribute__((unused)), char *domain __attribute__((unused)), unsigned long queueID __attribute__((unused)), int queue __attribute__((unused))) { return -1; } int -QDBRemoveID(void *handle, unsigned long queueID) +QDBRemoveID(void *handle __attribute__((unused)), unsigned long queueID __attribute__((unused))) { return -1; } int -QDBRemoveDomain(void *handle, unsigned char *domain) +QDBRemoveDomain(void *handle __attribute__((unused)), char *domain __attribute__((unused))) { return -1; } int -QDBSearchID(void *handle, unsigned long queueID) +QDBSearchID(void *handle __attribute__((unused)), unsigned long queueID __attribute__((unused))) { return -1; } int -QDBSearchDomain(void *handle, unsigned char *domain) +QDBSearchDomain(void *handle __attribute__((unused)), char *domain __attribute__((unused))) { return -1; } int -QDBQuery(void *handle, unsigned char *query, QDBQueryResults *results) +QDBQuery(void *handle __attribute__((unused)), char *query __attribute__((unused)), QDBQueryResults *results __attribute__((unused))) { return -1; } int -QDBDump(unsigned char *domain) +QDBDump(char *domain __attribute__((unused))) { return -1; } @@ -93,4 +93,3 @@ void QDBSummarizeQueue(void) { } - diff --git a/src/agents/queue/domain.h b/src/agents/queue/domain.h index 82edd8f..d58c0d2 100644 --- a/src/agents/queue/domain.h +++ b/src/agents/queue/domain.h @@ -11,13 +11,13 @@ int QDBStartup(int minimum, int maximum); void QDBShutdown(void); void *QDBHandleAlloc(void); void QDBHandleRelease(void *handle); -int QDBAdd(void *handle, unsigned char *domain, unsigned long queueID, int queue); +int QDBAdd(void *handle, char *domain, unsigned long queueID, int queue); int QDBRemoveID(void *handle, unsigned long queueID); -int QDBRemoveDomain(void *handle, unsigned char *domain); +int QDBRemoveDomain(void *handle, char *domain); int QDBSearchID(void *handle, unsigned long queueID); -int QDBSearchDomain(void *handle, unsigned char *domain); -int QDBQuery(void *handle, unsigned char *query, QDBQueryResults *results); -int QDBDump(unsigned char *domain); +int QDBSearchDomain(void *handle, char *domain); +int QDBQuery(void *handle, char *query, QDBQueryResults *results); +int QDBDump(char *domain); void QDBSummarizeQueue(void); #endif diff --git a/src/agents/queue/mime.c b/src/agents/queue/mime.c index 853ba51..0fd77af 100644 --- a/src/agents/queue/mime.c +++ b/src/agents/queue/mime.c @@ -30,7 +30,7 @@ #include static BOOL -ParseDispositionParameters(unsigned char *ptr, MIMEPartStruct *part) +ParseDispositionParameters(char *ptr, MIMEPartStruct *part) { RFC2231ParamStruct *rfc2231Name = NULL; @@ -67,7 +67,7 @@ ParseDispositionParameters(unsigned char *ptr, MIMEPartStruct *part) } __inline static BOOL -ParseTypeParameters(unsigned char *ptr, MIMEPartStruct *part) +ParseTypeParameters(char *ptr, MIMEPartStruct *part) { RFC2231ParamStruct *rfc2231Name = NULL; @@ -289,7 +289,7 @@ FreeMIME(MIMEReportStruct *report) } MIMEReportStruct * -ParseMIME(FILE *fh, unsigned long headerSize, unsigned long messageSize, long bodyLines, unsigned char *line, unsigned long lineSize) +ParseMIME(FILE *fh, unsigned long headerSize, unsigned long messageSize, long bodyLines, char *line, unsigned long lineSize) { int i; int j; @@ -305,12 +305,12 @@ ParseMIME(FILE *fh, unsigned long headerSize, unsigned long messageSize, long bo unsigned long lastHeaderStart = 0; unsigned long lastHeaderLen = 0; unsigned long contentHeaderLen = 0; - unsigned char *ptr; - unsigned char *start; - unsigned char *end; - unsigned char *header = NULL; - unsigned char *currentSeparator = NULL; - unsigned char *allocatedSeparator = NULL; + char *ptr; + char *start; + char *end; + char *header = NULL; + char *currentSeparator = NULL; + char *allocatedSeparator = NULL; BOOL lastInHeader = TRUE; BOOL inHeader = TRUE; BOOL skip = FALSE; @@ -322,7 +322,7 @@ ParseMIME(FILE *fh, unsigned long headerSize, unsigned long messageSize, long bo partsAllocated = MIME_STRUCT_ALLOC; part = (MIMEPartStruct *)MemMalloc((sizeof(MIMEPartStruct) * partsAllocated) + sizeof(MIMEReportStruct)); - header = (unsigned char *)MemMalloc(sizeof(unsigned char)*MAX_HEADER); + header = (char *)MemMalloc(sizeof(char)*MAX_HEADER); if (!part || !header) { if (part) { MemFree(part); @@ -462,7 +462,7 @@ ParseMIME(FILE *fh, unsigned long headerSize, unsigned long messageSize, long bo doingDigest = TRUE; } ParseTypeParameters(end + 1, &part[partNo]); - if (part[partNo].separator) { + if (part[partNo].separator[0] != '\0') { currentSeparator = part[partNo].separator; } partNo++; diff --git a/src/agents/queue/mime.h b/src/agents/queue/mime.h index 1d5da02..893186c 100644 --- a/src/agents/queue/mime.h +++ b/src/agents/queue/mime.h @@ -29,12 +29,12 @@ #include "connio.h" typedef struct _MIMEPartStruct { - unsigned char separator[MIME_SEPARATOR_LEN+1]; - unsigned char type[MIME_TYPE_LEN+1]; - unsigned char subtype[MIME_SUBTYPE_LEN+1]; - unsigned char charset[MIME_CHARSET_LEN+1]; - unsigned char encoding[MIME_ENCODING_LEN+1]; - unsigned char name[MIME_NAME_LEN+1]; + char separator[MIME_SEPARATOR_LEN+1]; + char type[MIME_TYPE_LEN+1]; + char subtype[MIME_SUBTYPE_LEN+1]; + char charset[MIME_CHARSET_LEN+1]; + char encoding[MIME_ENCODING_LEN+1]; + char name[MIME_NAME_LEN+1]; long headerStart; unsigned long headerLen; unsigned long start; @@ -60,6 +60,6 @@ typedef struct _MIMEReportStruct { int SendMIME(Connection *conn, MIMEReportStruct *report); int SendMimeDetails(Connection *conn, MIMEReportStruct *report); void FreeMIME(MIMEReportStruct *report); -MIMEReportStruct *ParseMIME(FILE *fh, unsigned long headerSize, unsigned long messageSize, long bodyLines, unsigned char *line, unsigned long lineSize); +MIMEReportStruct *ParseMIME(FILE *fh, unsigned long headerSize, unsigned long messageSize, long bodyLines, char *line, unsigned long lineSize); #endif diff --git a/src/agents/queue/queue.c b/src/agents/queue/queue.c index c0585bd..f51dba6 100644 --- a/src/agents/queue/queue.c +++ b/src/agents/queue/queue.c @@ -25,6 +25,33 @@ #include "queue.h" #include "mime.h" #include "messages.h" + +static BOOL +QueueFormatBuffer(char *destination, size_t destinationSize, + const char *format, ...) +{ + int written; + va_list arguments; + + if (!destination || destinationSize == 0) { + return FALSE; + } + + va_start(arguments, format); + written = vsnprintf(destination, destinationSize, format, arguments); + va_end(arguments); + + if (written < 0 || (size_t)written >= destinationSize) { + destination[0] = '\0'; + Log(LOG_ERROR, "queue: Refusing an overlong formatted path or identifier"); + return FALSE; + } + + return TRUE; +} + +#define QueueFormat(destination, ...) \ + QueueFormatBuffer((destination), sizeof(destination), __VA_ARGS__) #include "domain.h" #include @@ -139,9 +166,9 @@ int IntCmp (const unsigned long *s1, const unsigned long *s2) { static int PDBSearch(char *doc, char *searchString) { - unsigned char *w[MAX_CHARS_IN_PDBSEARCH], *textStart, *textEnd, *text, *end; + char *w[MAX_CHARS_IN_PDBSEARCH], *textStart, *textEnd, *text, *end; int wl[MAX_CHARS_IN_PDBSEARCH]; - unsigned char expression[MAX_CHARS_IN_PDBSEARCH]; + char expression[MAX_CHARS_IN_PDBSEARCH]; long word, words = 0, i,len; int found = 0; @@ -295,7 +322,7 @@ AddPushAgent(QueueClient *client, char *address, int port, int queue, - unsigned char *identifier) + char *identifier) { /* since the sorting doesn't always pass in the int or the struct the same every time i need to put the int in a struct */ QueueList tempQueue; @@ -467,9 +494,15 @@ DeliverToStore(NMAPConnections *list, { NMAPConnection *nmap = NULL; int ccode = 0; - unsigned char line[CONN_BUFSIZE + 1]; + char line[CONN_BUFSIZE + 1]; BOOL new; + UNUSED_PARAMETER(type); + UNUSED_PARAMETER(from); + UNUSED_PARAMETER(authFrom); + UNUSED_PARAMETER(filename); + UNUSED_PARAMETER(messageFlags); + if ((ccode = GetNMAPConnection(list, address, &nmap, &new)) < 0) { return DELIVER_TRY_LATER; } @@ -501,7 +534,7 @@ EndStoreDelivery(NMAPConnections *list) { int ccode; long index; - unsigned char line[CONN_BUFSIZE + 1]; + char line[CONN_BUFSIZE + 1]; for (index = 0; index < list->used; index++) { NMAPConnection *nmap = &list->connections[index]; @@ -536,7 +569,7 @@ ProcessQueueEntryCleanUp(unsigned long id, MIMEReportStruct *report) } static BOOL -ProcessQueueEntry(unsigned char *entryIn) +ProcessQueueEntry(char *entryIn) { int i; int queue; @@ -554,21 +587,21 @@ ProcessQueueEntry(unsigned char *entryIn) unsigned int iter_p=0; unsigned long dSize = 0; unsigned long entryID = 0; - unsigned char *ptr; - unsigned char *ptr2 = NULL; - unsigned char *cur; - unsigned char *next; - unsigned char *limit; - unsigned char *qDate; - unsigned char *qFlags; - unsigned char *qID; - unsigned char *qAddress; - unsigned char *qFrom; - unsigned char *qEnvelope = NULL; - unsigned char path[XPL_MAX_PATH + 1] = ""; - unsigned char path2[XPL_MAX_PATH + 1]; - unsigned char line[CONN_BUFSIZE + 1] = ""; - unsigned char entry[15]; + char *ptr; + char *ptr2 = NULL; + char *cur; + char *next; + char *limit; + char *qDate; + char *qFlags; + char *qID; + char *qAddress; + char *qFrom; + char *qEnvelope = NULL; + char path[XPL_MAX_PATH + 1] = ""; + char path2[XPL_MAX_PATH + 1]; + char line[CONN_BUFSIZE + 1] = ""; + char entry[15]; BOOL keep = TRUE; BOOL bounce = FALSE; time_t date; @@ -603,7 +636,7 @@ StartOver: Log(LOG_DEBUG, "Processing entry %ld on queue %d", entryID, queue); if (SpoolEntryIDLock(entryID)) { - sprintf(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); FOPEN_CHECK(fh, path, "r+b"); } else { ProcessQueueEntryCleanUp(0, report); @@ -630,17 +663,17 @@ StartOver: qAddress = NULL; qFrom = NULL; - sprintf(path, "%s/w%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/w%s.%03d", Conf.spoolPath, entry, queue); FOPEN_CHECK(newFH, path, "wb"); - sprintf(path, "%s/c%s.%03d",Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/c%s.%03d",Conf.spoolPath, entry, queue); FOPEN_CHECK(temp, path, "rb"); if (newFH && (stat(path, &sb) == 0) && (sb.st_size > 8) - && ((qEnvelope = (unsigned char *)MemMalloc(sb.st_size + 1)) != NULL) + && ((qEnvelope = (char *)MemMalloc(sb.st_size + 1)) != NULL) && (temp) - && (fread(qEnvelope, sizeof(unsigned char), sb.st_size, temp) == (size_t)sb.st_size)) { + && (fread(qEnvelope, sizeof(char), sb.st_size, temp) == (size_t)sb.st_size)) { /* Sort the control file as follows: QUEUE_DATE QUEUE_FLAGS @@ -731,23 +764,23 @@ StartOver: } if (qDate && qFrom && count) { - fwrite(qDate, sizeof(unsigned char), qDateLength, newFH); + fwrite(qDate, sizeof(char), qDateLength, newFH); if (qFlags) { - fwrite(qFlags, sizeof(unsigned char), qFlagsLength, newFH); + fwrite(qFlags, sizeof(char), qFlagsLength, newFH); } else { - fwrite(QUEUES_FLAGS"0\r\n", sizeof(unsigned char), 4, newFH); + fwrite(QUEUES_FLAGS"0\r\n", sizeof(char), 4, newFH); } if (qID) { - fwrite(qID, sizeof(unsigned char), qIDLength, newFH); + fwrite(qID, sizeof(char), qIDLength, newFH); } if (qAddress) { - fwrite(qAddress, sizeof(unsigned char), qAddressLength, newFH); + fwrite(qAddress, sizeof(char), qAddressLength, newFH); } - fwrite(qFrom, sizeof(unsigned char), qFromLength, newFH); + fwrite(qFrom, sizeof(char), qFromLength, newFH); } else { /* fixme - if a new queue entry has at least QUEUE_FROM but no recipients we should bounce the message rather @@ -756,10 +789,10 @@ StartOver: UNLINK_CHECK(path); - sprintf(path, "%s/w%s.%03d",Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/w%s.%03d",Conf.spoolPath, entry, queue); UNLINK_CHECK(path); - sprintf(path, "%s/d%s.msg",Conf.spoolPath, entry); + QueueFormat(path, "%s/d%s.msg",Conf.spoolPath, entry); UNLINK_CHECK(path); MemFree(qEnvelope); @@ -790,7 +823,7 @@ StartOver: switch (*cur) { case QUEUE_BOUNCE: case QUEUE_CALENDAR_LOCAL: { - fwrite(cur, sizeof(unsigned char), next - cur, newFH); + fwrite(cur, sizeof(char), next - cur, newFH); break; } @@ -807,9 +840,9 @@ StartOver: } /* alias this user */ - unsigned char buffer[1024]; /* FIXME: is this too big? */ - unsigned char *addrptr; - unsigned char *flags = strchr(ptr2+1,' '); /* the original flags */ + char buffer[1024]; /* FIXME: is this too big? */ + char *addrptr; + char *flags = strchr(ptr2+1,' '); /* the original flags */ int cnt=0; aliasing(cur+1, &cnt, buffer); @@ -857,7 +890,7 @@ StartOver: case QUEUE_THIRD_PARTY: default: { - fwrite(cur, sizeof(unsigned char), next - cur, newFH); + fwrite(cur, sizeof(char), next - cur, newFH); break; } } @@ -871,7 +904,7 @@ StartOver: UNLINK_CHECK(path); - sprintf(path2, "%s/w%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path2, "%s/w%s.%03d", Conf.spoolPath, entry, queue); RENAME_CHECK(path2, path); break; @@ -882,7 +915,7 @@ StartOver: } if (!newFH) { - sprintf(path, "%s/w%s.%03d",Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/w%s.%03d",Conf.spoolPath, entry, queue); LogFailureF("File open failure. Entry %ld, path %s", entryID, path); } else if (!qEnvelope) { LogFailureF("Out of memory. Entry %ld, size %ld", entryID, sb.st_size); @@ -906,7 +939,7 @@ StartOver: Log(LOG_WARNING, "Write error in queue"); - sprintf(path, "%s/w%s.%03d",Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/w%s.%03d",Conf.spoolPath, entry, queue); UNLINK_CHECK(path); ProcessQueueEntryCleanUp(entryID, report); @@ -918,11 +951,11 @@ StartOver: case Q_OUTGOING: { if (date < time(NULL) - Conf.maxLinger) { /* We move it to the Q_RTS queue and the linger code there will bounce it for us */ - sprintf(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); - sprintf(path2, "%s/c%s.%03d", Conf.spoolPath, entry, Q_RTS); + QueueFormat(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path2, "%s/c%s.%03d", Conf.spoolPath, entry, Q_RTS); RENAME_CHECK(path, path2); - sprintf(path, "%03d%s", Q_RTS, entry); + QueueFormat(path, "%03d%s", Q_RTS, entry); SpoolEntryIDUnlock(entryID); entryIn = MemStrdup(path); goto StartOver; @@ -934,29 +967,30 @@ StartOver: } /* Fall-through to Q_DELIVER */ + __attribute__((fallthrough)); } case Q_DELIVER: { int status; unsigned long messageFlags; unsigned long flags = DSN_FAILURE | DSN_HEADER | DSN_BODY; - unsigned char *mailbox; - unsigned char *preMailboxDelim = NULL; - unsigned char *postMailboxDelim = NULL; - unsigned char recipient[MAXEMAILNAMESIZE + 1]; - unsigned char sender[MAXEMAILNAMESIZE + 1]; - unsigned char authenticatedSender[MAXEMAILNAMESIZE + 1]; - unsigned char messageID[MAXEMAILNAMESIZE + 1]; + char *mailbox; + char *preMailboxDelim = NULL; + char *postMailboxDelim = NULL; + char recipient[MAXEMAILNAMESIZE + 1]; + char sender[MAXEMAILNAMESIZE + 1]; + char authenticatedSender[MAXEMAILNAMESIZE + 1]; + char messageID[MAXEMAILNAMESIZE + 1]; char dataFilename[XPL_MAX_PATH]; NMAPConnections list = { 0, }; data = NULL; saddr.sin_addr.s_addr = 0; - sprintf(dataFilename, "d%s.msg", entry); + QueueFormat(dataFilename, "d%s.msg", entry); if (!dSize) { - sprintf(path, "%s/%s", Conf.spoolPath, dataFilename); + QueueFormat(path, "%s/%s", Conf.spoolPath, dataFilename); if (stat(path, &sb)) { ProcessQueueEntryCleanUp(entryID, report); return(TRUE); @@ -968,10 +1002,10 @@ StartOver: keep = FALSE; bounce = FALSE; - sprintf(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); FOPEN_CHECK(fh, path, "rb"); - sprintf(path, "%s/w%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/w%s.%03d", Conf.spoolPath, entry, queue); FOPEN_CHECK(newFH, path, "wb"); if (!fh || !newFH) { @@ -983,7 +1017,7 @@ StartOver: FCLOSE_CHECK(newFH); } - sprintf(path, "%s/w%s.%03d",Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/w%s.%03d",Conf.spoolPath, entry, queue); UNLINK_CHECK(path); ProcessQueueEntryCleanUp(entryID, report); @@ -1034,7 +1068,7 @@ StartOver: FOPEN_CHECK(data, path, "rb"); if (!data) { FCLOSE_CHECK(fh); - sprintf(path, "%s/w%s.%03d",Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/w%s.%03d",Conf.spoolPath, entry, queue); FCLOSE_CHECK(newFH); UNLINK_CHECK(path); ProcessQueueEntryCleanUp(entryID, report); @@ -1129,18 +1163,18 @@ StartOver: ptr++; } } - /**** FALLTHROUGH ****/ + __attribute__((fallthrough)); case QUEUE_RECIP_LOCAL: { /* Local */ struct sockaddr_in siaddr; if (!data) { - sprintf(path, "%s/d%s.msg", Conf.spoolPath, entry); + QueueFormat(path, "%s/d%s.msg", Conf.spoolPath, entry); FOPEN_CHECK(data, path, "rb"); if (!data) { FCLOSE_CHECK(fh); FCLOSE_CHECK(newFH); - sprintf(path, "%s/w%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/w%s.%03d", Conf.spoolPath, entry, queue); UNLINK_CHECK(path); ProcessQueueEntryCleanUp(entryID, report); @@ -1316,29 +1350,29 @@ StartOver: FCLOSE_CHECK(newFH); FCLOSE_CHECK(fh); if (bounce) { - unsigned char Path2[XPL_MAX_PATH+1]; + char Path2[XPL_MAX_PATH+1]; /* First, rename the work file into a control file */ - sprintf(path, "%s/c%s.%03d",Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/c%s.%03d",Conf.spoolPath, entry, queue); UNLINK_CHECK(path); - sprintf(Path2, "%s/w%s.%03d",Conf.spoolPath, entry, queue); + QueueFormat(Path2, "%s/w%s.%03d",Conf.spoolPath, entry, queue); RENAME_CHECK(Path2, path); if (!data) { - sprintf(path, "%s/d%s.msg",Conf.spoolPath, entry); + QueueFormat(path, "%s/d%s.msg",Conf.spoolPath, entry); FOPEN_CHECK(data, path, "rb"); } else { fseek(data, 0, SEEK_SET); } - sprintf(path, "%s/c%s.%03d",Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/c%s.%03d",Conf.spoolPath, entry, queue); FOPEN_CHECK(fh, path, "rb"); if (fh && data && 0 == HandleDSN(data, fh)) { /* Now bounce the thing */ fseek(fh, 0, SEEK_SET); - sprintf(path, "%s/w%s.%03d",Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/w%s.%03d",Conf.spoolPath, entry, queue); FOPEN_CHECK(newFH, path, "wb"); if (newFH) { /* Now remove the bounced entries */ @@ -1374,21 +1408,21 @@ StartOver: FCLOSE_CHECK(data); } if (keep) { - unsigned char Path2[XPL_MAX_PATH+1]; + char Path2[XPL_MAX_PATH+1]; - sprintf(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); UNLINK_CHECK(path); - sprintf(Path2, "%s/w%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(Path2, "%s/w%s.%03d", Conf.spoolPath, entry, queue); RENAME_CHECK(Path2, path); } else { - sprintf(path, "%s/w%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/w%s.%03d", Conf.spoolPath, entry, queue); UNLINK_CHECK(path); - sprintf(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); UNLINK_CHECK(path); - sprintf(path, "%s/d%s.msg", Conf.spoolPath, entry); + QueueFormat(path, "%s/d%s.msg", Conf.spoolPath, entry); UNLINK_CHECK(path); XplSafeDecrement(Queue.queuedLocal); @@ -1402,7 +1436,7 @@ StartOver: } if (Agent.agent.state < BONGO_AGENT_STATE_STOPPING) { - sprintf(path, "%s/d%s.msg", Conf.spoolPath, entry); + QueueFormat(path, "%s/d%s.msg", Conf.spoolPath, entry); } else { ProcessQueueEntryCleanUp(entryID, report); return(TRUE); @@ -1423,7 +1457,7 @@ StartOver: for (iter_p=0; iter_p < CurrentQueue->pools->len; iter_p++) { QueuePoolList *CurrentPool = g_array_index(CurrentQueue->pools, QueuePoolList *, iter_p); - sprintf(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); stat(path, &sb); FOPEN_CHECK(fh, path, "rb"); if (!fh) { @@ -1474,7 +1508,7 @@ StartOver: FCLOSE_CHECK(fh); fh=NULL; - sprintf(client->entry.workQueue, "%03d-%s", queue, entry); + QueueFormat(client->entry.workQueue, "%03d-%s", queue, entry); client->entry.workQueue[3] = '\0'; ConnWriteStr(client->conn, "6021 Get busy!\r\n"); ConnFlush(client->conn); @@ -1489,7 +1523,7 @@ StartOver: Log(LOG_DEBUG, "Ran into queue resend [C%s.%03d] Last Command: %s", entry, queue, client->buffer); FCLOSE_CHECK(client->entry.work); client->entry.work = NULL; - sprintf(client->path,"%s/w%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(client->path,"%s/w%s.%03d", Conf.spoolPath, entry, queue); remove(client->path); } @@ -1560,11 +1594,11 @@ StartOver: } Log(LOG_DEBUG, "Moving %ld from queue %d to queue %d", entryID, queue, i); - sprintf(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); - sprintf(path2, "%s/c%s.%03d", Conf.spoolPath, entry, i); + QueueFormat(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path2, "%s/c%s.%03d", Conf.spoolPath, entry, i); RENAME_CHECK(path, path2); - sprintf(path, "%03d%s", i, entry); + QueueFormat(path, "%03d%s", i, entry); SpoolEntryIDUnlock(entryID); entryIn = MemStrdup(path); @@ -1574,7 +1608,7 @@ StartOver: case Q_OUTGOING: { /* Check if there are any recipients left */ - sprintf(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); if ((handle = QDBHandleAlloc()) != NULL) { QDBRemoveID(handle, entryID); @@ -1636,7 +1670,7 @@ StartOver: /* Note that we make sure to keep the content of path pointing to the control file; this way we only need one sprintf */ if (bounce) { /* Call the bouncing code */ - sprintf(path2, "%s/d%s.msg", Conf.spoolPath, entry); + QueueFormat(path2, "%s/d%s.msg", Conf.spoolPath, entry); FOPEN_CHECK(data, path2, "rb"); FOPEN_CHECK(fh, path, "rb"); @@ -1645,7 +1679,7 @@ StartOver: /* If we're not keeping the file, we can ignore its contents */ if (keep) { - sprintf(path2, "%s/w%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path2, "%s/w%s.%03d", Conf.spoolPath, entry, queue); FOPEN_CHECK(newFH, path2, "wb"); if (newFH) { /* Now remove the bounced entries */ @@ -1696,7 +1730,7 @@ StartOver: UNLINK_CHECK(path); - sprintf(path, "%s/d%s.msg",Conf.spoolPath, entry); + QueueFormat(path, "%s/d%s.msg",Conf.spoolPath, entry); UNLINK_CHECK(path); break; @@ -1713,13 +1747,13 @@ StartOver: QDBHandleRelease(handle); } - sprintf(path, "%s/w%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/w%s.%03d", Conf.spoolPath, entry, queue); FOPEN_CHECK(newFH, path, "wb"); - sprintf(path, "%s/d%s.msg", Conf.spoolPath, entry); + QueueFormat(path, "%s/d%s.msg", Conf.spoolPath, entry); FOPEN_CHECK(data, path, "rb"); - sprintf(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); FOPEN_CHECK(fh, path, "rb"); if (fh && newFH && data) { @@ -1770,7 +1804,7 @@ StartOver: /* Still got path from above */ UNLINK_CHECK(path); - sprintf(path2, "%s/w%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path2, "%s/w%s.%03d", Conf.spoolPath, entry, queue); RENAME_CHECK(path2, path); FOPEN_CHECK(fh, path, "rb"); @@ -1783,10 +1817,10 @@ StartOver: FCLOSE_CHECK(data); - sprintf(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); + QueueFormat(path, "%s/c%s.%03d", Conf.spoolPath, entry, queue); UNLINK_CHECK(path); - sprintf(path, "%s/d%s.msg", Conf.spoolPath, entry); + QueueFormat(path, "%s/d%s.msg", Conf.spoolPath, entry); UNLINK_CHECK(path); XplSafeDecrement(Queue.queuedLocal); @@ -1808,11 +1842,11 @@ StartOver: return(TRUE); } else { count = 0; - sprintf(path, "%s/c%s.%03d", Conf.spoolPath, entry, Q_RTS); - sprintf(path2, "%s/c%s.%03d", Conf.spoolPath, entry, Q_INCOMING); + QueueFormat(path, "%s/c%s.%03d", Conf.spoolPath, entry, Q_RTS); + QueueFormat(path2, "%s/c%s.%03d", Conf.spoolPath, entry, Q_INCOMING); RENAME_CHECK(path, path2); - sprintf(path, "%03d%s", Q_INCOMING, entry); + QueueFormat(path, "%03d%s", Q_INCOMING, entry); } SpoolEntryIDUnlock(entryID); @@ -1846,17 +1880,17 @@ CreateDSNMessage(FILE *data, FILE *control, FILE *rtsData, FILE *rtsControl, BOO unsigned long maxBodySize; unsigned long received = 0; unsigned long handling; - unsigned char *ptr; - unsigned char *ptr2; - unsigned char *transcript; - unsigned char timeLine[80]; - unsigned char line[CONN_BUFSIZE + 1]; - unsigned char postmaster[MAXEMAILNAMESIZE + 1]; - unsigned char sender[MAXEMAILNAMESIZE + 1]; - unsigned char aSender[MAXEMAILNAMESIZE + 1] = ""; - unsigned char recipient[MAXEMAILNAMESIZE + 1] = ""; - unsigned char oRecipient[MAXEMAILNAMESIZE + 1] = ""; - unsigned char envID[128] = ""; + char *ptr; + char *ptr2; + char *transcript; + char timeLine[80]; + char line[CONN_BUFSIZE + 1]; + char postmaster[MAXEMAILNAMESIZE + 1]; + char sender[MAXEMAILNAMESIZE + 1]; + char aSender[MAXEMAILNAMESIZE + 1] = ""; + char recipient[MAXEMAILNAMESIZE + 1] = ""; + char oRecipient[MAXEMAILNAMESIZE + 1] = ""; + char envID[128] = ""; BOOL mBounce=FALSE; BOOL header; time_t now; @@ -1932,7 +1966,7 @@ CreateDSNMessage(FILE *data, FILE *control, FILE *rtsData, FILE *rtsControl, BOO return(FALSE); } - sprintf(postmaster, "%s@%s", BongoGlobals.postmaster, BongoGlobals.hostname); + QueueFormat(postmaster, "%s@%s", BongoGlobals.postmaster, BongoGlobals.hostname); handling = Conf.bounceHandling; /* We're guaranteed to have a recipient */ @@ -1947,7 +1981,7 @@ CreateDSNMessage(FILE *data, FILE *control, FILE *rtsData, FILE *rtsControl, BOO if (MDBRead(dn, MSGSRV_A_POSTMASTER, vs) > 0) { ptr2 = strrchr(vs->Value[0], '\\'); if (ptr2) { - sprintf(postmaster, "%s@%s", ptr2 + 1, ptr + 1); + QueueFormat(postmaster, "%s@%s", ptr2 + 1, ptr + 1); } MDBFreeValues(vs); @@ -2259,14 +2293,14 @@ CreateDSNMessage(FILE *data, FILE *control, FILE *rtsData, FILE *rtsControl, BOO if (flags & DSN_HEADER) { if ((line[0] == 'F') && (line[1] == 'r') && (line[4] == ' ')) { - fwrite(">", sizeof(unsigned char), 1, rtsData); + fwrite(">", sizeof(char), 1, rtsData); } - bodySize += fwrite(line, sizeof(unsigned char), strlen(line), rtsData); + bodySize += fwrite(line, sizeof(char), strlen(line), rtsData); } } else { if (flags & DSN_BODY) { - bodySize += fwrite(line, sizeof(unsigned char), strlen(line), rtsData); + bodySize += fwrite(line, sizeof(char), strlen(line), rtsData); if (bodySize > maxBodySize) { break; } @@ -2288,11 +2322,11 @@ CreateDSNMessage(FILE *data, FILE *control, FILE *rtsData, FILE *rtsControl, BOO XplRWReadLockAcquire(&Conf.lock); - if (Conf.bounceHandling & BOUNCE_RETURN) { + if (handling & BOUNCE_RETURN) { fprintf(rtsControl, QUEUES_RECIP_REMOTE"%s %s 0\r\n", sender, sender); } - if (Conf.bounceHandling & BOUNCE_CC_POSTMASTER) { + if (handling & BOUNCE_CC_POSTMASTER) { fprintf(rtsControl, QUEUES_RECIP_REMOTE"%s %s 0\r\n", postmaster, postmaster); } @@ -2305,7 +2339,7 @@ static int HandleDSN(FILE *data, FILE *control) { unsigned long id; - unsigned char path[XPL_MAX_PATH + 1]; + char path[XPL_MAX_PATH + 1]; FILE *rtsControl = NULL; FILE *rtsData = NULL; XplThreadID threadID; @@ -2314,7 +2348,7 @@ HandleDSN(FILE *data, FILE *control) id = Queue.queueID++ & ((1 << 28) - 1); XplMutexUnlock(Queue.queueIDLock); - sprintf(path, "%s/c%07lx.%03d", Conf.spoolPath, id, Q_INCOMING); + QueueFormat(path, "%s/c%07lx.%03d", Conf.spoolPath, id, Q_INCOMING); /* Create control file */ FOPEN_CHECK(rtsControl, path,"wb"); @@ -2326,12 +2360,12 @@ HandleDSN(FILE *data, FILE *control) SpoolEntryIDLock(id); /* Create data file */ - sprintf(path, "%s/d%07lx.msg", Conf.spoolPath, id); + QueueFormat(path, "%s/d%07lx.msg", Conf.spoolPath, id); FOPEN_CHECK(rtsData, path, "wb"); if (!rtsData) { fprintf (stderr, "could not open rtsData\n"); FCLOSE_CHECK(rtsControl); - sprintf(path, "%s/d%07lx.msg", Conf.spoolPath, id); + QueueFormat(path, "%s/d%07lx.msg", Conf.spoolPath, id); UNLINK_CHECK(path); return -1; } @@ -2340,10 +2374,10 @@ HandleDSN(FILE *data, FILE *control) FCLOSE_CHECK(rtsControl); FCLOSE_CHECK(rtsData); - sprintf(path, "%s/d%07lx.msg", Conf.spoolPath, id); + QueueFormat(path, "%s/d%07lx.msg", Conf.spoolPath, id); UNLINK_CHECK(path); - sprintf(path, "%s/c%07lx.%03d", Conf.spoolPath, id, Q_INCOMING); + QueueFormat(path, "%s/c%07lx.%03d", Conf.spoolPath, id, Q_INCOMING); UNLINK_CHECK(path); SpoolEntryIDUnlock(id); @@ -2356,7 +2390,7 @@ HandleDSN(FILE *data, FILE *control) FCLOSE_CHECK(rtsData); SpoolEntryIDUnlock(id); - sprintf(path, "%03d%lx",Q_INCOMING, id); + QueueFormat(path, "%03d%lx",Q_INCOMING, id); if (XplSafeRead(Queue.activeWorkers) < Conf.maxConcurrentWorkers) { XplBeginCountedThread(&threadID, ProcessQueueEntry, STACKSPACE_Q, MemStrdup(path), id, Queue.activeWorkers); @@ -2368,12 +2402,13 @@ HandleDSN(FILE *data, FILE *control) static void CheckQueue(void *queueIn) { + UNUSED_PARAMETER(queueIn); int ccode; int burst; int count; unsigned long found; unsigned long handled; - unsigned char path[XPL_MAX_PATH + 1]; + char path[XPL_MAX_PATH + 1]; time_t now; XplDir *dirP; XplDir *dirEntry; @@ -2421,7 +2456,7 @@ CheckQueue(void *queueIn) } found++; - sprintf(path, "%.3s%.7s", dirEntry->d_nameDOS + 9, dirEntry->d_nameDOS + 1); + QueueFormat(path, "%.3s%.7s", dirEntry->d_nameDOS + 9, dirEntry->d_nameDOS + 1); /* skip recently-touched messages that are in the outgoing * queue (queue 7) */ @@ -2514,7 +2549,7 @@ CreateQueueThreads(BOOL failed) unsigned long count; unsigned long total; unsigned long current; - unsigned char path[XPL_MAX_PATH + 1]; + char path[XPL_MAX_PATH + 1]; FILE *control = NULL; FILE *killFile = NULL; XplDir *dirP; @@ -2531,11 +2566,11 @@ CreateQueueThreads(BOOL failed) Log(LOG_DEBUG, "Verifying queue integrity, quick mode."); } - sprintf(path, "%s", Conf.spoolPath); + QueueFormat(path, "%s", Conf.spoolPath); dirP = XplOpenDir(path); - sprintf(path, "%s/fragfile", MsgGetDir(MSGAPI_DIR_DBF, NULL, 0)); + QueueFormat(path, "%s/fragfile", MsgGetDir(MSGAPI_DIR_DBF, NULL, 0)); FOPEN_CHECK(killFile, path, "wb"); if (!killFile) { if (dirP) { @@ -2570,8 +2605,15 @@ CreateQueueThreads(BOOL failed) count++; } else if (failed) { - sprintf(path, "%s/d%s", Conf.spoolPath, dirEntry->d_nameDOS + 1); - sprintf(path + strlen(path) - 3, "msg"); + QueueFormat(path, "%s/d%s", Conf.spoolPath, dirEntry->d_nameDOS + 1); + if (strlen(path) >= 3) { + size_t extensionOffset = strlen(path) - 3; + QueueFormatBuffer(path + extensionOffset, + sizeof(path) - extensionOffset, + "msg"); + } else { + path[0] = '\0'; + } if (access(path, 0) == 0) { /* Check for matching D file */ if (dirEntry->d_size <= 0) { @@ -2611,9 +2653,16 @@ CreateQueueThreads(BOOL failed) count += 2; } else if (failed) { - sprintf(path, "%s/c%s", Conf.spoolPath, dirEntry->d_nameDOS + 1); + QueueFormat(path, "%s/c%s", Conf.spoolPath, dirEntry->d_nameDOS + 1); for (i = 0; i < 10; i++) { - sprintf(path + strlen(path) - 3, "%03d", i); + if (strlen(path) >= 3) { + size_t extensionOffset = strlen(path) - 3; + QueueFormatBuffer(path + extensionOffset, + sizeof(path) - extensionOffset, + "%03d", i); + } else { + path[0] = '\0'; + } if (access(path, 0)==0) { break; } @@ -2650,7 +2699,7 @@ CreateQueueThreads(BOOL failed) XplCloseDir(dirP); } - sprintf(path, "%s", Conf.spoolPath); + QueueFormat(path, "%s", Conf.spoolPath); dirP = XplOpenDir(path); if (killFile) { @@ -2660,7 +2709,7 @@ CreateQueueThreads(BOOL failed) Log(LOG_DEBUG, "Queue integrity check complete, now cleaning irrelevant entries."); - sprintf(path, "%s/fragfile", MsgGetDir(MSGAPI_DIR_DBF, NULL, 0)); + QueueFormat(path, "%s/fragfile", MsgGetDir(MSGAPI_DIR_DBF, NULL, 0)); FOPEN_CHECK(killFile, path, "rb"); if (!killFile) { Log(LOG_ERROR, "Could not re-open killfile."); @@ -2678,13 +2727,13 @@ CreateQueueThreads(BOOL failed) FCLOSE_CHECK(killFile); } - sprintf(path, "%s/killfile", MsgGetDir(MSGAPI_DIR_DBF, NULL, 0)); + QueueFormat(path, "%s/killfile", MsgGetDir(MSGAPI_DIR_DBF, NULL, 0)); UNLINK_CHECK(path); XplCloseDir(dirP); if (failed) { - sprintf(path, "%s", Conf.spoolPath); + QueueFormat(path, "%s", Conf.spoolPath); dirP = XplOpenDir(path); if (dirP) { @@ -2701,7 +2750,7 @@ CreateQueueThreads(BOOL failed) current++; if ((dirEntry->d_nameDOS[0] & 0xDF) == 'C' ) { - sprintf(path, "%s/c%s", Conf.spoolPath, dirEntry->d_nameDOS + 1); + QueueFormat(path, "%s/c%s", Conf.spoolPath, dirEntry->d_nameDOS + 1); FOPEN_CHECK(control, path, "r+b"); if (!control) { continue; @@ -2734,13 +2783,12 @@ CommandQaddm(void *param) int ccode; int result; unsigned long flags = 0; - unsigned char *sender; - unsigned char *authSender; - unsigned char *recipient; - unsigned char *mailbox; - unsigned char *queue; - unsigned char *ptr; - unsigned char *ptr2; + char *sender; + char *authSender; + char *recipient; + char *mailbox; + char *ptr; + char *ptr2; struct stat sb; FILE *data = NULL; QueueClient *client = (QueueClient *)param; @@ -2771,8 +2819,6 @@ CommandQaddm(void *param) *mailbox++ = '\0'; *ptr2++ = '\0'; - queue = ptr; - ptr += 4; flags = atol(ptr2); @@ -2782,7 +2828,7 @@ CommandQaddm(void *param) result = DELIVER_FAILURE; - sprintf(client->path, "%s/d%s.msg", Conf.spoolPath, ptr); + QueueFormat(client->path, "%s/d%s.msg", Conf.spoolPath, ptr); if (stat(client->path, &sb) == 0) { FOPEN_CHECK(data, client->path, "rb"); } @@ -2791,8 +2837,10 @@ CommandQaddm(void *param) struct sockaddr_in saddr; if (MsgAuthFindUser(recipient) == 0) { - if (MsgAuthGetUserStore(recipient, &saddr) == TRUE) { - result = DeliverToStore(&list, &saddr, NMAP_DOCTYPE_MAIL, sender, authSender, client->path, data, sb.st_size, recipient, mailbox, 0); + if (MsgAuthGetUserStore(recipient, &saddr) == 0) { + result = DeliverToStore(&list, &saddr, NMAP_DOCTYPE_MAIL, + sender, authSender, client->path, data, + sb.st_size, recipient, mailbox, flags); EndStoreDelivery(&list); } } @@ -2820,9 +2868,9 @@ CommandQaddq(void *param) unsigned long len; unsigned long start; unsigned long length; - unsigned char *ptr; - unsigned char *ptr2; - unsigned char *ptr3; + char *ptr; + char *ptr2; + char *ptr3; FILE *data = NULL; QueueClient *client = (QueueClient *)param; @@ -2864,7 +2912,7 @@ CommandQaddq(void *param) /* fixme - LockQueueEntry? LockQueueEntry(ptr+4, atoi(ptr)); */ - sprintf(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); + QueueFormat(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); FOPEN_CHECK(data, client->path, "rb"); if (data) { fseek(data, start, SEEK_SET); @@ -2876,13 +2924,13 @@ CommandQaddq(void *param) while (!feof(data) && !ferror(data) && (length != 0)) { if (length > CONN_BUFSIZE) { - len = fread(client->line, sizeof(unsigned char), CONN_BUFSIZE, data); + len = fread(client->line, sizeof(char), CONN_BUFSIZE, data); } else { - len = fread(client->line, sizeof(unsigned char), length, data); + len = fread(client->line, sizeof(char), length, data); } if (len) { - fwrite(client->line, sizeof(unsigned char), len, client->entry.data); + fwrite(client->line, sizeof(char), len, client->entry.data); length -= len; } } @@ -2905,7 +2953,7 @@ CommandQabrt(void *param) FCLOSE_CHECK(client->entry.control); client->entry.control = NULL; - sprintf(client->path,"%s/c%07lx.in", Conf.spoolPath, client->entry.id); + QueueFormat(client->path,"%s/c%07lx.in", Conf.spoolPath, client->entry.id); UNLINK_CHECK(client->path); } @@ -2913,7 +2961,7 @@ CommandQabrt(void *param) FCLOSE_CHECK(client->entry.data); client->entry.data = NULL; - sprintf(client->path,"%s/d%07lx.msg",Conf.spoolPath, client->entry.id); + QueueFormat(client->path,"%s/d%07lx.msg",Conf.spoolPath, client->entry.id); UNLINK_CHECK(client->path); } if (client->entry.work) { @@ -2921,7 +2969,7 @@ CommandQabrt(void *param) client->entry.work = NULL; if (client->entry.workQueue[0] != '\0') { - sprintf(client->path,"%s/w%s.%s",Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); + QueueFormat(client->path,"%s/w%s.%s",Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); UNLINK_CHECK(client->path); } } @@ -2937,7 +2985,7 @@ CommandQbody(void *param) { int ccode; unsigned long count = 0; - unsigned char *ptr; + char *ptr; struct stat sb; FILE *data = NULL; QueueClient *client = (QueueClient *)param; @@ -2957,7 +3005,7 @@ CommandQbody(void *param) return(ConnWrite(client->conn, MSG3010BADARGC, sizeof(MSG3010BADARGC) - 1)); } - sprintf(client->path,"%s/d%s.msg", Conf.spoolPath, ptr + 4); + QueueFormat(client->path,"%s/d%s.msg", Conf.spoolPath, ptr + 4); if (stat(client->path, &sb) == 0) { FOPEN_CHECK(data, client->path, "rb"); } @@ -2994,9 +3042,9 @@ CommandQbraw(void *param) unsigned long start; unsigned long size; unsigned long count = 0; - unsigned char *ptr; - unsigned char *ptr2; - unsigned char *ptr3; + char *ptr; + char *ptr2; + char *ptr3; struct stat sb; FILE *data = NULL; QueueClient *client = (QueueClient *)param; @@ -3024,7 +3072,7 @@ CommandQbraw(void *param) return(ConnWrite(client->conn, MSG3010BADARGC, sizeof(MSG3010BADARGC) - 1)); } - sprintf(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); + QueueFormat(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); if (stat(client->path, &sb) == 0) { FOPEN_CHECK(data, client->path, "rb"); } @@ -3067,8 +3115,8 @@ CommandQcopy(void *param) unsigned long id; unsigned long len; unsigned long target; - unsigned char *ptr; - unsigned char *ptr2; + char *ptr; + char *ptr2; FILE *source = NULL; QueueClient *client = (QueueClient *)param; @@ -3118,7 +3166,7 @@ CommandQcopy(void *param) /* fixme - LockQueueEntry? LockQueueEntry(ptr+4, atoi(ptr)); */ - sprintf(client->path, "%s/d%s.msg",Conf.spoolPath, ptr + 4); + QueueFormat(client->path, "%s/d%s.msg",Conf.spoolPath, ptr + 4); FOPEN_CHECK(source, client->path, "rb"); if (source) { XplMutexLock(Queue.queueIDLock); @@ -3133,7 +3181,7 @@ CommandQcopy(void *param) client->entry.id = id; - sprintf(client->path, "%s/c%07lx.in", Conf.spoolPath, id); + QueueFormat(client->path, "%s/c%07lx.in", Conf.spoolPath, id); FOPEN_CHECK(client->entry.control, client->path, "wb"); if (client->entry.control) { fprintf(client->entry.control, QUEUES_DATE"%lu\r\n", time(NULL)); @@ -3143,13 +3191,13 @@ CommandQcopy(void *param) return(ConnWrite(client->conn, MSG5221SPACELOW, sizeof(MSG5221SPACELOW) - 1)); } - sprintf(client->path, "%s/d%07lx.msg", Conf.spoolPath, id); + QueueFormat(client->path, "%s/d%07lx.msg", Conf.spoolPath, id); FOPEN_CHECK(client->entry.data, client->path, "wb"); while (!feof(source) && !ferror(source)) { - len = fread(client->line, sizeof(unsigned char), CONN_BUFSIZE, source); + len = fread(client->line, sizeof(char), CONN_BUFSIZE, source); if (len) { - fwrite(client->line, sizeof(unsigned char), len, client->entry.data); + fwrite(client->line, sizeof(char), len, client->entry.data); } } @@ -3166,7 +3214,7 @@ CommandQcrea(void *param) { unsigned long id; unsigned long target; - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; if (Agent.flags & QUEUE_AGENT_DISK_SPACE_LOW) { @@ -3198,7 +3246,7 @@ CommandQcrea(void *param) id = Queue.queueID++ & ((1 << 28) - 1); XplMutexUnlock(Queue.queueIDLock); - sprintf(client->path, "%s/c%07lx.in", Conf.spoolPath, id); + QueueFormat(client->path, "%s/c%07lx.in", Conf.spoolPath, id); FOPEN_CHECK(client->entry.control, client->path, "wb"); if (client->entry.control) { fprintf(client->entry.control, QUEUES_DATE"%lu\r\n", time(NULL)); @@ -3206,13 +3254,13 @@ CommandQcrea(void *param) return(ConnWrite(client->conn, MSG5221SPACELOW, sizeof(MSG5221SPACELOW) - 1)); } - sprintf(client->path, "%s/d%07lx.msg", Conf.spoolPath, id); + QueueFormat(client->path, "%s/d%07lx.msg", Conf.spoolPath, id); FOPEN_CHECK(client->entry.data, client->path, "wb"); if (client->entry.data) { client->entry.id = id; } else { FCLOSE_CHECK(client->entry.control); - sprintf(client->path, "%s/c%07lx.in", Conf.spoolPath, id); + QueueFormat(client->path, "%s/c%07lx.in", Conf.spoolPath, id); UNLINK_CHECK(client->path); return(ConnWrite(client->conn, MSG5221SPACELOW, sizeof(MSG5221SPACELOW) - 1)); @@ -3225,7 +3273,7 @@ CommandQcrea(void *param) int CommandQdele(void *param) { - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; ptr = client->buffer + 5; @@ -3247,10 +3295,10 @@ CommandQdele(void *param) /* fixme - LockQueueEntry? LockQueueEntry(ptr+4, atoi(ptr)); */ - sprintf(client->path, "%s/c%s.%s", Conf.spoolPath, ptr + 4, ptr); + QueueFormat(client->path, "%s/c%s.%s", Conf.spoolPath, ptr + 4, ptr); UNLINK_CHECK(client->path); - sprintf(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); + QueueFormat(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); UNLINK_CHECK(client->path); /* FIXME: close out the file handles? */ @@ -3265,7 +3313,7 @@ int CommandQdone(void *param) { int ccode; - unsigned char path[XPL_MAX_PATH + 1]; + char path[XPL_MAX_PATH + 1]; struct stat sb; struct stat sb1; struct stat sb2; @@ -3277,8 +3325,8 @@ CommandQdone(void *param) FCLOSE_CHECK(client->entry.work); client->entry.work = NULL; if (client->entry.workQueue[0] != '\0') { - sprintf(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); - sprintf(path, "%s/c%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); + QueueFormat(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); + QueueFormat(path, "%s/c%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); if ((stat(client->path, &sb1) == 0) && (stat(path, &sb2) == 0)) { UNLINK_CHECK(path); RENAME_CHECK(client->path, path); @@ -3331,9 +3379,9 @@ int CommandQgrep(void *param) { int ccode; - int length; + size_t length; char *field; - unsigned char *ptr; + char *ptr; BOOL found = FALSE; FILE *data = NULL; QueueClient *client = (QueueClient *)param; @@ -3356,7 +3404,7 @@ CommandQgrep(void *param) return(ConnWrite(client->conn, MSG3010BADARGC, sizeof(MSG3010BADARGC) - 1)); } - sprintf(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); + QueueFormat(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); FOPEN_CHECK(data, client->path, "rb"); if (data) { length = strlen(field); @@ -3404,7 +3452,7 @@ CommandQhead(void *param) { int ccode; unsigned long count = 0; - unsigned char *ptr; + char *ptr; FILE *data = NULL; QueueClient *client = (QueueClient *)param; @@ -3424,7 +3472,7 @@ CommandQhead(void *param) return(ConnWrite(client->conn, MSG3010BADARGC, sizeof(MSG3010BADARGC) - 1)); } - sprintf(client->path,"%s/d%s.msg", Conf.spoolPath, ptr+4); + QueueFormat(client->path,"%s/d%s.msg", Conf.spoolPath, ptr+4); FOPEN_CHECK(data, client->path, "rb"); if (data) { while (!feof(data) && !ferror(data)) { @@ -3457,7 +3505,7 @@ int CommandQinfo(void *param) { unsigned long count = 0; - unsigned char *ptr; + char *ptr; struct stat sb; FILE *data = NULL; QueueClient *client = (QueueClient *)param; @@ -3478,7 +3526,7 @@ CommandQinfo(void *param) return(ConnWrite(client->conn, MSG3010BADARGC, sizeof(MSG3010BADARGC) - 1)); } - sprintf(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); + QueueFormat(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); if (stat(client->path, &sb) == 0) { FOPEN_CHECK(data, client->path, "rb"); } @@ -3509,13 +3557,13 @@ CommandQinfo(void *param) int CommandQmodFrom(void *param) { - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; ptr = client->buffer + 9; if (!client->entry.work) { - sprintf(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); + QueueFormat(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); FOPEN_CHECK(client->entry.work, client->path, "wb"); if (!client->entry.work) { return(0); @@ -3534,13 +3582,13 @@ CommandQmodFrom(void *param) int CommandQmodFlags(void *param) { - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; ptr = client->buffer + 10; if (!client->entry.work) { - sprintf(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); + QueueFormat(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); FOPEN_CHECK(client->entry.work, client->path, "wb"); if (!client->entry.work) { return(0); @@ -3559,13 +3607,13 @@ CommandQmodFlags(void *param) int CommandQmodLocal(void *param) { - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; ptr = client->buffer + 10; if (!client->entry.work) { - sprintf(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); + QueueFormat(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); FOPEN_CHECK(client->entry.work, client->path, "wb"); if (!client->entry.work) { return(0); @@ -3584,13 +3632,13 @@ CommandQmodLocal(void *param) int CommandQmodMailbox(void *param) { - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; ptr = client->buffer + 12; if (!client->entry.work) { - sprintf(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); + QueueFormat(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); FOPEN_CHECK(client->entry.work, client->path, "wb"); if (!client->entry.work) { return(0); @@ -3609,13 +3657,13 @@ CommandQmodMailbox(void *param) int CommandQmodRaw(void *param) { - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; ptr = client->buffer + 8; if (!client->entry.work) { - sprintf(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); + QueueFormat(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); FOPEN_CHECK(client->entry.work, client->path, "wb"); if (!client->entry.work) { return(0); @@ -3634,13 +3682,13 @@ CommandQmodRaw(void *param) int CommandQmodTo(void *param) { - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; ptr = client->buffer + 7; if (!client->entry.work) { - sprintf(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); + QueueFormat(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); FOPEN_CHECK(client->entry.work, client->path, "wb"); if (!client->entry.work) { return(0); @@ -3662,7 +3710,7 @@ CommandQmime(void *param) int ccode; long size; unsigned long i; - unsigned char *ptr; + char *ptr; struct stat sb; FILE *data = NULL; MIMEReportStruct *report; @@ -3686,7 +3734,7 @@ CommandQmime(void *param) if (!client->entry.report) { /* Find the message size */ - sprintf(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); + QueueFormat(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); if (stat(client->path, &sb) == 0) { FOPEN_CHECK(data, client->path, "rb"); } @@ -3741,9 +3789,9 @@ int CommandQmove(void *param) { int ccode; - unsigned char *ptr; - unsigned char *ptr2; - unsigned char path[XPL_MAX_PATH + 1]; + char *ptr; + char *ptr2; + char path[XPL_MAX_PATH + 1]; struct stat sb; QueueClient *client = (QueueClient *)param; @@ -3765,15 +3813,15 @@ CommandQmove(void *param) return(ConnWrite(client->conn, MSG3010BADARGC, sizeof(MSG3010BADARGC) - 1)); } - sprintf(client->path, "%s/c%s.%s", Conf.spoolPath, ptr + 4, ptr); + QueueFormat(client->path, "%s/c%s.%s", Conf.spoolPath, ptr + 4, ptr); if (stat(client->path, &sb) == 0) { - sprintf(path, "%s/c%s.%03d", Conf.spoolPath, ptr + 4, atoi(ptr)); + QueueFormat(path, "%s/c%s.%03d", Conf.spoolPath, ptr + 4, atoi(ptr)); RENAME_CHECK(client->path, path); ccode = ConnWrite(client->conn, MSG1000OK, sizeof(MSG1000OK) - 1); } else { - sprintf(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); + QueueFormat(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); UNLINK_CHECK(client->path); ccode = ConnWrite(client->conn, MSG4224NOENTRY, sizeof(MSG4224NOENTRY) - 1); @@ -3787,8 +3835,8 @@ CommandQrcp(void *param) { int ccode; unsigned long id; - unsigned char *ptr; - unsigned char path[XPL_MAX_PATH + 1]; + char *ptr; + char path[XPL_MAX_PATH + 1]; FILE *source = NULL; QueueClient *client = (QueueClient *)param; @@ -3817,31 +3865,31 @@ CommandQrcp(void *param) return(ConnWrite(client->conn, MSG3010BADARGC, sizeof(MSG3010BADARGC) - 1)); } - sprintf(client->path, "%s/c%07lx.in", Conf.spoolPath, id); + QueueFormat(client->path, "%s/c%07lx.in", Conf.spoolPath, id); FOPEN_CHECK(client->entry.control, client->path,"wb"); if (client->entry.control) { fprintf(client->entry.control, QUEUES_DATE"%lu\r\n", time(NULL)); - sprintf(client->path, "%s/d%07lx.msg", Conf.spoolPath, id); + QueueFormat(client->path, "%s/d%07lx.msg", Conf.spoolPath, id); FOPEN_CHECK(client->entry.data, client->path, "wb"); if (client->entry.data) { - sprintf(client->path, "%s/d%07lx.msg", Conf.spoolPath, client->entry.id); + QueueFormat(client->path, "%s/d%07lx.msg", Conf.spoolPath, client->entry.id); FOPEN_CHECK(source, client->path, "rb"); while (!feof(source) && !ferror(source)) { - ccode = fread(client->line, sizeof(unsigned char), CONN_BUFSIZE, source); + ccode = fread(client->line, sizeof(char), CONN_BUFSIZE, source); if (ccode) { - fwrite(client->line, sizeof(unsigned char), ccode, client->entry.data); + fwrite(client->line, sizeof(char), ccode, client->entry.data); } } FCLOSE_CHECK(source); - sprintf(client->path,"%s/c%07lx.in",Conf.spoolPath, client->entry.id); - sprintf(path, "%s/c%07lx.%03ld", Conf.spoolPath, client->entry.id, client->entry.target); + QueueFormat(client->path,"%s/c%07lx.in",Conf.spoolPath, client->entry.id); + QueueFormat(path, "%s/c%07lx.%03ld", Conf.spoolPath, client->entry.id, client->entry.target); RENAME_CHECK(client->path, path); - sprintf(client->path, "%03ld%lx", client->entry.target, client->entry.id); + QueueFormat(client->path, "%03ld%lx", client->entry.target, client->entry.id); if (XplSafeRead(Queue.activeWorkers) < Conf.maxConcurrentWorkers) { XplThreadID threadID; @@ -3867,14 +3915,14 @@ CommandQrcp(void *param) FCLOSE_CHECK(client->entry.control); client->entry.control = NULL; - sprintf(client->path, "%s/c%07lx.in", Conf.spoolPath, id); + QueueFormat(client->path, "%s/c%07lx.in", Conf.spoolPath, id); UNLINK_CHECK(client->path); } - sprintf(client->path, "%s/c%07lx.in", Conf.spoolPath, client->entry.id); + QueueFormat(client->path, "%s/c%07lx.in", Conf.spoolPath, client->entry.id); FOPEN_CHECK(client->entry.control, path, "a+b"); - sprintf(client->path,"%s/d%07lx.msg", Conf.spoolPath, client->entry.id); + QueueFormat(client->path,"%s/d%07lx.msg", Conf.spoolPath, client->entry.id); FOPEN_CHECK(client->entry.data, path, "a+b"); if (client->entry.data && client->entry.control) { @@ -3900,8 +3948,8 @@ int CommandQretr(void *param) { int ccode; - unsigned char *ptr; - unsigned char *ptr2; + char *ptr; + char *ptr2; struct stat sb; FILE *data = NULL; QueueClient *client = (QueueClient *)param; @@ -3927,7 +3975,7 @@ CommandQretr(void *param) } if (XplStrCaseCmp(ptr2, "INFO") == 0) { - sprintf(client->path, "%s/c07%s.%s", Conf.spoolPath, ptr + 4, ptr); + QueueFormat(client->path, "%s/c07%s.%s", Conf.spoolPath, ptr + 4, ptr); if ((stat(client->path, &sb) == 0) && sb.st_size) { FOPEN_CHECK(data, client->path, "rb"); } @@ -3938,7 +3986,7 @@ CommandQretr(void *param) return(ConnWrite(client->conn, MSG4224CANTREAD, sizeof(MSG4224CANTREAD) - 1)); } } else if (XplStrCaseCmp(ptr2, "MESSAGE") == 0) { - sprintf(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); + QueueFormat(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); if (stat(client->path, &sb) == 0) { FOPEN_CHECK(data, client->path, "rb"); } @@ -3964,9 +4012,9 @@ CommandQretr(void *param) int CommandQrts(void *param) { - unsigned char *recipient; - unsigned char *oRecipient; - unsigned char *flags; + char *recipient; + char *oRecipient; + char *flags; QueueClient *client = (QueueClient *)param; @@ -3978,7 +4026,7 @@ CommandQrts(void *param) recipient = client->buffer + 4; if (!client->entry.work) { - sprintf(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); + QueueFormat(client->path, "%s/w%s.%s", Conf.spoolPath, client->entry.workQueue + 4, client->entry.workQueue); FOPEN_CHECK(client->entry.work, client->path, "wb"); if (!client->entry.work) { return(0); @@ -4006,8 +4054,8 @@ CommandQrun(void *param) int ccode; unsigned long target; unsigned long id; - unsigned char *ptr; - unsigned char path[XPL_MAX_PATH + 1]; + char *ptr; + char path[XPL_MAX_PATH + 1]; QueueClient *client = (QueueClient *)param; ptr = client->buffer + 4; @@ -4024,15 +4072,15 @@ CommandQrun(void *param) return(ConnWrite(client->conn, MSG4000CANTUNLOCKENTRY, sizeof(MSG4000CANTUNLOCKENTRY) - 1)); } - sprintf(client->path,"%s/c%07lx.in",Conf.spoolPath, client->entry.id); - sprintf(path, "%s/c%07lx.%03ld", Conf.spoolPath, client->entry.id, client->entry.target); + QueueFormat(client->path,"%s/c%07lx.in",Conf.spoolPath, client->entry.id); + QueueFormat(path, "%s/c%07lx.%03ld", Conf.spoolPath, client->entry.id, client->entry.target); RENAME_CHECK(client->path, path); XplSafeIncrement(Queue.queuedLocal); ccode = ConnWriteF(client->conn, "1000 %03ld-%lx OK\r\n", client->entry.target, client->entry.id); - sprintf(client->path, "%03ld%07lx", client->entry.target, client->entry.id); + QueueFormat(client->path, "%03ld%07lx", client->entry.target, client->entry.id); client->entry.id = 0; } else if ((*ptr++ == ' ') && (!isspace(*ptr)) @@ -4049,7 +4097,7 @@ CommandQrun(void *param) ccode = ConnWriteF(client->conn, "1000 %03ld-%lx OK\r\n", target, id); - sprintf(client->path, "%03ld%07lx", target, id); + QueueFormat(client->path, "%03ld%07lx", target, id); } else { return(ConnWrite(client->conn, MSG3010BADARGC, sizeof(MSG3010BADARGC) - 1)); } @@ -4074,7 +4122,7 @@ int CommandQsrchDomain(void *param) { int ccode; - unsigned char *ptr; + char *ptr; void *handle = NULL; QueueClient *client = (QueueClient *)param; @@ -4114,7 +4162,7 @@ CommandQsrchHeader(void *param) int length; char *field; char *content; - unsigned char *ptr; + char *ptr; BOOL fieldFound=FALSE; BOOL contentFound=FALSE; FILE *data = NULL; @@ -4148,7 +4196,7 @@ CommandQsrchHeader(void *param) } - sprintf(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); + QueueFormat(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); FOPEN_CHECK(data, client->path, "rb"); if (data) { while (!feof(data) && !ferror(data)) { @@ -4203,9 +4251,8 @@ int CommandQsrchBody(void *param) { int ccode; - int length; char *content; - unsigned char *ptr; + char *ptr; BOOL found = FALSE; FILE *data = NULL; QueueClient *client = (QueueClient *)param; @@ -4226,12 +4273,11 @@ CommandQsrchBody(void *param) ptr[3] = '\0'; *content++ = '\0'; - length = strlen(content); } else { return(ConnWrite(client->conn, MSG3010BADARGC, sizeof(MSG3010BADARGC) - 1)); } - sprintf(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); + QueueFormat(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); FOPEN_CHECK(data, client->path, "rb"); if (data) { while (!feof(data) && !ferror(data)) { @@ -4246,7 +4292,7 @@ CommandQsrchBody(void *param) /* fixme - this doesn't search across buffer bondaries! */ while (!feof(data) && !ferror(data)) { - if (fread(client->line, sizeof(unsigned char), CONN_BUFSIZE, data) > 0) { + if (fread(client->line, sizeof(char), CONN_BUFSIZE, data) > 0) { if (PDBSearch(client->line, content) == FALSE) { continue; } @@ -4275,13 +4321,12 @@ CommandQsrchBraw(void *param) { int ccode; int read; - int length; unsigned long start; unsigned long end; - unsigned char *ptr; - unsigned char *startPtr; - unsigned char *endPtr; - unsigned char *content; + char *ptr; + char *startPtr; + char *endPtr; + char *content; BOOL found = FALSE; FILE *data = NULL; QueueClient *client = (QueueClient *)param; @@ -4312,12 +4357,11 @@ CommandQsrchBraw(void *param) start = atol(startPtr); end = atol(endPtr); - length = strlen(content); } else { return(ConnWrite(client->conn, MSG3010BADARGC, sizeof(MSG3010BADARGC) - 1)); } - sprintf(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); + QueueFormat(client->path, "%s/d%s.msg", Conf.spoolPath, ptr + 4); FOPEN_CHECK(data, client->path, "rb"); if (data) { while (!feof(data) && !ferror(data)) { @@ -4336,9 +4380,9 @@ CommandQsrchBraw(void *param) /* fixme - this doesn't search across buffer bondaries! */ while ((end > 0) && !feof(data) && !ferror(data)) { if (end >= CONN_BUFSIZE) { - read = fread(client->line, sizeof(unsigned char), CONN_BUFSIZE, data); + read = fread(client->line, sizeof(char), CONN_BUFSIZE, data); } else { - read = fread(client->line, sizeof(unsigned char), end, data); + read = fread(client->line, sizeof(char), end, data); } if (read) { @@ -4371,7 +4415,7 @@ CommandQsrchBraw(void *param) int CommandQstorAddress(void *param) { - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; if (!client->entry.data || !client->entry.control) { @@ -4394,7 +4438,7 @@ CommandQstorAddress(void *param) int CommandQstorCal(void *param) { - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; if (!client->entry.data || !client->entry.control) { @@ -4417,7 +4461,7 @@ CommandQstorCal(void *param) int CommandQstorFlags(void *param) { - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; if (!client->entry.data || !client->entry.control) { @@ -4440,7 +4484,7 @@ CommandQstorFlags(void *param) int CommandQstorFrom(void *param) { - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; if (!client->entry.data || !client->entry.control) { @@ -4462,7 +4506,7 @@ CommandQstorFrom(void *param) int CommandQstorLocal(void *param) { - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; if (!client->entry.data || !client->entry.control) { @@ -4487,9 +4531,9 @@ CommandQstorMessage(void *param) { int ccode; long count; - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; - unsigned char TimeBuf[80]; + char TimeBuf[80]; if (!client->entry.data || !client->entry.control) { return(ConnWrite(client->conn, MSG4260NOQENTRY, sizeof(MSG4260NOQENTRY) - 1)); @@ -4549,7 +4593,7 @@ CommandQstorMessage(void *param) int CommandQstorRaw(void *param) { - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; if (!client->entry.data || !client->entry.control) { @@ -4572,7 +4616,7 @@ CommandQstorRaw(void *param) int CommandQstorTo(void *param) { - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; if (!client->entry.data || !client->entry.control) { @@ -4598,8 +4642,8 @@ CommandQwait(void *param) int ccode; int port; int queue; - unsigned char *ptr; - unsigned char *identifier; + char *ptr; + char *identifier; QueueClient *client = (QueueClient *)param; ptr = client->buffer + 5; diff --git a/src/agents/queue/queue.h b/src/agents/queue/queue.h index 5067f6d..b3f471b 100644 --- a/src/agents/queue/queue.h +++ b/src/agents/queue/queue.h @@ -36,7 +36,7 @@ typedef struct _QueuePushClient { unsigned long usageCount; unsigned long errorCount; - unsigned char identifier[101]; + char identifier[101]; } QueuePushClient; typedef struct _QueueList { @@ -46,7 +46,7 @@ typedef struct _QueueList { } QueueList; typedef struct _QueuePoolList { - unsigned char identifier[101]; /* each queue agent should have a unique identifier so that pooling will work correctly */ + char identifier[101]; /* each queue agent should have a unique identifier so that pooling will work correctly */ AddressPool pool; } QueuePoolList; diff --git a/src/agents/queue/queued.c b/src/agents/queue/queued.c index f92608f..99690b0 100644 --- a/src/agents/queue/queued.c +++ b/src/agents/queue/queued.c @@ -36,7 +36,7 @@ #include "domain.h" #include "messages.h" -QueueAgent Agent = { {0, }, }; +QueueAgent Agent = {0}; static int CommandAuth(void *param); static int CommandPass(void *param); @@ -47,60 +47,60 @@ static int CommandDomainLocation(void *param); int aliasFindFunc(const void *str, const void *node); static ProtocolCommand authCommands[] = { - { NMAP_AUTH_COMMAND, NMAP_AUTH_HELP, sizeof(NMAP_AUTH_COMMAND) - 1, CommandAuth, NULL, NULL }, - { NMAP_HELP_COMMAND, NMAP_HELP_HELP, sizeof(NMAP_HELP_COMMAND) - 1, CommandHelp, NULL, NULL }, - { NMAP_PASS_COMMAND, NMAP_PASS_HELP, sizeof(NMAP_PASS_COMMAND) - 1, CommandPass, NULL, NULL }, - { NMAP_QUIT_COMMAND, NMAP_QUIT_HELP, sizeof(NMAP_QUIT_COMMAND) - 1, CommandQuit, NULL, NULL }, - { NMAP_ADDRESS_RESOLVE_COMMAND, NMAP_ADDRESS_RESOLVE_HELP, sizeof(NMAP_ADDRESS_RESOLVE_COMMAND) -1, CommandAddressResolve, NULL, NULL }, - { NMAP_DOMAIN_LOCATION_COMMAND, NMAP_DOMAIN_LOCATION_HELP, sizeof(NMAP_DOMAIN_LOCATION_COMMAND) -1, CommandDomainLocation, NULL, NULL }, - { NULL, NULL, 0, NULL, NULL, NULL } + { NMAP_AUTH_COMMAND, NMAP_AUTH_HELP, sizeof(NMAP_AUTH_COMMAND) - 1, CommandAuth, NULL, NULL, NULL, BlackCommand }, + { NMAP_HELP_COMMAND, NMAP_HELP_HELP, sizeof(NMAP_HELP_COMMAND) - 1, CommandHelp, NULL, NULL, NULL, BlackCommand }, + { NMAP_PASS_COMMAND, NMAP_PASS_HELP, sizeof(NMAP_PASS_COMMAND) - 1, CommandPass, NULL, NULL, NULL, BlackCommand }, + { NMAP_QUIT_COMMAND, NMAP_QUIT_HELP, sizeof(NMAP_QUIT_COMMAND) - 1, CommandQuit, NULL, NULL, NULL, BlackCommand }, + { NMAP_ADDRESS_RESOLVE_COMMAND, NMAP_ADDRESS_RESOLVE_HELP, sizeof(NMAP_ADDRESS_RESOLVE_COMMAND) -1, CommandAddressResolve, NULL, NULL, NULL, BlackCommand }, + { NMAP_DOMAIN_LOCATION_COMMAND, NMAP_DOMAIN_LOCATION_HELP, sizeof(NMAP_DOMAIN_LOCATION_COMMAND) -1, CommandDomainLocation, NULL, NULL, NULL, BlackCommand }, + { NULL, NULL, 0, NULL, NULL, NULL, NULL, BlackCommand } }; static ProtocolCommand commands[] = { - { NMAP_HELP_COMMAND, NMAP_HELP_HELP, sizeof(NMAP_HELP_COMMAND) - 1, CommandHelp, NULL, NULL }, - { NMAP_QADDM_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QADDM_COMMAND) - 1, CommandQaddm, NULL, NULL }, - { NMAP_QADDQ_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QADDQ_COMMAND) - 1, CommandQaddq, NULL, NULL }, - { NMAP_QABRT_COMMAND, NMAP_QABRT_HELP, sizeof(NMAP_QABRT_COMMAND) - 1, CommandQabrt, NULL, NULL }, - { NMAP_QBODY_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QBODY_COMMAND) - 1, CommandQbody, NULL, NULL }, - { NMAP_QBRAW_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QBRAW_COMMAND) - 1, CommandQbraw, NULL, NULL }, - { NMAP_QCOPY_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QCOPY_COMMAND) - 1, CommandQcopy, NULL, NULL }, - { NMAP_QCREA_COMMAND, NMAP_QCREA_HELP, sizeof(NMAP_QCREA_COMMAND) - 1, CommandQcrea, NULL, NULL }, - { NMAP_QDELE_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QDELE_COMMAND) - 1, CommandQdele, NULL, NULL }, - { NMAP_QDONE_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QDONE_COMMAND) - 1, CommandQdone, NULL, NULL }, - { NMAP_QDSPC_COMMAND, NMAP_QDSPC_HELP, sizeof(NMAP_QDSPC_COMMAND) - 1, CommandQdspc, NULL, NULL }, - { NMAP_QGREP_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QGREP_COMMAND) - 1, CommandQgrep, NULL, NULL }, - { NMAP_QHEAD_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QHEAD_COMMAND) - 1, CommandQhead, NULL, NULL }, - { NMAP_QINFO_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QINFO_COMMAND) - 1, CommandQinfo, NULL, NULL }, - { NMAP_QMOD_FROM_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMOD_FROM_COMMAND) - 1, CommandQmodFrom, NULL, NULL }, - { NMAP_QMOD_FLAGS_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMOD_FLAGS_COMMAND) - 1, CommandQmodFlags, NULL, NULL }, - { NMAP_QMOD_LOCAL_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMOD_LOCAL_COMMAND) - 1, CommandQmodLocal, NULL, NULL }, - { NMAP_QMOD_MAILBOX_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMOD_MAILBOX_COMMAND) - 1, CommandQmodMailbox, NULL, NULL }, - { NMAP_QMOD_RAW_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMOD_RAW_COMMAND) - 1, CommandQmodRaw, NULL, NULL }, - { NMAP_QMOD_TO_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMOD_TO_COMMAND) - 1, CommandQmodTo, NULL, NULL }, - { NMAP_QMIME_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMIME_COMMAND) - 1, CommandQmime, NULL, NULL }, - { NMAP_QMOVE_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMOVE_COMMAND) - 1, CommandQmove, NULL, NULL }, - { NMAP_QRCP_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QRCP_COMMAND) - 1, CommandQrcp, NULL, NULL }, - { NMAP_QRETR_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QRETR_COMMAND) - 1, CommandQretr, NULL, NULL }, - { NMAP_QRTS_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QRTS_COMMAND) - 1, CommandQrts, NULL, NULL }, - { NMAP_QRUN_COMMAND, NMAP_QRUN_HELP, sizeof(NMAP_QRUN_COMMAND) - 1, CommandQrun, NULL, NULL }, - { NMAP_QSRCH_DOMAIN_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSRCH_DOMAIN_COMMAND) - 1, CommandQsrchDomain, NULL, NULL }, - { NMAP_QSRCH_HEADER_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSRCH_HEADER_COMMAND) - 1, CommandQsrchHeader, NULL, NULL }, - { NMAP_QSRCH_BODY_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSRCH_BODY_COMMAND) - 1, CommandQsrchBody, NULL, NULL }, - { NMAP_QSRCH_BRAW_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSRCH_BRAW_COMMAND) - 1, CommandQsrchBraw, NULL, NULL }, - { NMAP_QSTOR_ADDRESS_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_ADDRESS_COMMAND) - 1, CommandQstorAddress, NULL, NULL }, - { NMAP_QSTOR_CAL_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_CAL_COMMAND) - 1, CommandQstorCal, NULL, NULL }, - { NMAP_QSTOR_FLAGS_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_FLAGS_COMMAND) - 1, CommandQstorFlags, NULL, NULL }, - { NMAP_QSTOR_FROM_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_FROM_COMMAND) - 1, CommandQstorFrom, NULL, NULL }, - { NMAP_QSTOR_LOCAL_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_LOCAL_COMMAND) - 1, CommandQstorLocal, NULL, NULL }, - { NMAP_QSTOR_MESSAGE_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_MESSAGE_COMMAND) - 1, CommandQstorMessage, NULL, NULL }, - { NMAP_QSTOR_RAW_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_RAW_COMMAND) - 1, CommandQstorRaw, NULL, NULL }, - { NMAP_QSTOR_TO_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_TO_COMMAND) - 1, CommandQstorTo, NULL, NULL }, - { NMAP_QUIT_COMMAND, NMAP_QUIT_HELP, sizeof(NMAP_QUIT_COMMAND) - 1, CommandQuit, NULL, NULL }, - { NMAP_QWAIT_COMMAND, NMAP_QWAIT_HELP, sizeof(NMAP_QWAIT_COMMAND) - 1, CommandQwait, NULL, NULL }, - { NMAP_QFLUSH_COMMAND, NMAP_QFLUSH_HELP, sizeof(NMAP_QFLUSH_COMMAND) -1, CommandQflush, NULL, NULL }, - { NMAP_ADDRESS_RESOLVE_COMMAND, NMAP_ADDRESS_RESOLVE_HELP, sizeof(NMAP_ADDRESS_RESOLVE_COMMAND) -1, CommandAddressResolve, NULL, NULL }, - { NMAP_DOMAIN_LOCATION_COMMAND, NMAP_DOMAIN_LOCATION_HELP, sizeof(NMAP_DOMAIN_LOCATION_COMMAND) -1, CommandDomainLocation, NULL, NULL }, - { NULL, NULL, 0, NULL, NULL, NULL } + { NMAP_HELP_COMMAND, NMAP_HELP_HELP, sizeof(NMAP_HELP_COMMAND) - 1, CommandHelp, NULL, NULL, NULL, BlackCommand }, + { NMAP_QADDM_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QADDM_COMMAND) - 1, CommandQaddm, NULL, NULL, NULL, BlackCommand }, + { NMAP_QADDQ_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QADDQ_COMMAND) - 1, CommandQaddq, NULL, NULL, NULL, BlackCommand }, + { NMAP_QABRT_COMMAND, NMAP_QABRT_HELP, sizeof(NMAP_QABRT_COMMAND) - 1, CommandQabrt, NULL, NULL, NULL, BlackCommand }, + { NMAP_QBODY_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QBODY_COMMAND) - 1, CommandQbody, NULL, NULL, NULL, BlackCommand }, + { NMAP_QBRAW_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QBRAW_COMMAND) - 1, CommandQbraw, NULL, NULL, NULL, BlackCommand }, + { NMAP_QCOPY_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QCOPY_COMMAND) - 1, CommandQcopy, NULL, NULL, NULL, BlackCommand }, + { NMAP_QCREA_COMMAND, NMAP_QCREA_HELP, sizeof(NMAP_QCREA_COMMAND) - 1, CommandQcrea, NULL, NULL, NULL, BlackCommand }, + { NMAP_QDELE_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QDELE_COMMAND) - 1, CommandQdele, NULL, NULL, NULL, BlackCommand }, + { NMAP_QDONE_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QDONE_COMMAND) - 1, CommandQdone, NULL, NULL, NULL, BlackCommand }, + { NMAP_QDSPC_COMMAND, NMAP_QDSPC_HELP, sizeof(NMAP_QDSPC_COMMAND) - 1, CommandQdspc, NULL, NULL, NULL, BlackCommand }, + { NMAP_QGREP_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QGREP_COMMAND) - 1, CommandQgrep, NULL, NULL, NULL, BlackCommand }, + { NMAP_QHEAD_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QHEAD_COMMAND) - 1, CommandQhead, NULL, NULL, NULL, BlackCommand }, + { NMAP_QINFO_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QINFO_COMMAND) - 1, CommandQinfo, NULL, NULL, NULL, BlackCommand }, + { NMAP_QMOD_FROM_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMOD_FROM_COMMAND) - 1, CommandQmodFrom, NULL, NULL, NULL, BlackCommand }, + { NMAP_QMOD_FLAGS_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMOD_FLAGS_COMMAND) - 1, CommandQmodFlags, NULL, NULL, NULL, BlackCommand }, + { NMAP_QMOD_LOCAL_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMOD_LOCAL_COMMAND) - 1, CommandQmodLocal, NULL, NULL, NULL, BlackCommand }, + { NMAP_QMOD_MAILBOX_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMOD_MAILBOX_COMMAND) - 1, CommandQmodMailbox, NULL, NULL, NULL, BlackCommand }, + { NMAP_QMOD_RAW_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMOD_RAW_COMMAND) - 1, CommandQmodRaw, NULL, NULL, NULL, BlackCommand }, + { NMAP_QMOD_TO_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMOD_TO_COMMAND) - 1, CommandQmodTo, NULL, NULL, NULL, BlackCommand }, + { NMAP_QMIME_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMIME_COMMAND) - 1, CommandQmime, NULL, NULL, NULL, BlackCommand }, + { NMAP_QMOVE_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QMOVE_COMMAND) - 1, CommandQmove, NULL, NULL, NULL, BlackCommand }, + { NMAP_QRCP_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QRCP_COMMAND) - 1, CommandQrcp, NULL, NULL, NULL, BlackCommand }, + { NMAP_QRETR_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QRETR_COMMAND) - 1, CommandQretr, NULL, NULL, NULL, BlackCommand }, + { NMAP_QRTS_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QRTS_COMMAND) - 1, CommandQrts, NULL, NULL, NULL, BlackCommand }, + { NMAP_QRUN_COMMAND, NMAP_QRUN_HELP, sizeof(NMAP_QRUN_COMMAND) - 1, CommandQrun, NULL, NULL, NULL, BlackCommand }, + { NMAP_QSRCH_DOMAIN_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSRCH_DOMAIN_COMMAND) - 1, CommandQsrchDomain, NULL, NULL, NULL, BlackCommand }, + { NMAP_QSRCH_HEADER_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSRCH_HEADER_COMMAND) - 1, CommandQsrchHeader, NULL, NULL, NULL, BlackCommand }, + { NMAP_QSRCH_BODY_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSRCH_BODY_COMMAND) - 1, CommandQsrchBody, NULL, NULL, NULL, BlackCommand }, + { NMAP_QSRCH_BRAW_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSRCH_BRAW_COMMAND) - 1, CommandQsrchBraw, NULL, NULL, NULL, BlackCommand }, + { NMAP_QSTOR_ADDRESS_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_ADDRESS_COMMAND) - 1, CommandQstorAddress, NULL, NULL, NULL, BlackCommand }, + { NMAP_QSTOR_CAL_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_CAL_COMMAND) - 1, CommandQstorCal, NULL, NULL, NULL, BlackCommand }, + { NMAP_QSTOR_FLAGS_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_FLAGS_COMMAND) - 1, CommandQstorFlags, NULL, NULL, NULL, BlackCommand }, + { NMAP_QSTOR_FROM_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_FROM_COMMAND) - 1, CommandQstorFrom, NULL, NULL, NULL, BlackCommand }, + { NMAP_QSTOR_LOCAL_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_LOCAL_COMMAND) - 1, CommandQstorLocal, NULL, NULL, NULL, BlackCommand }, + { NMAP_QSTOR_MESSAGE_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_MESSAGE_COMMAND) - 1, CommandQstorMessage, NULL, NULL, NULL, BlackCommand }, + { NMAP_QSTOR_RAW_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_RAW_COMMAND) - 1, CommandQstorRaw, NULL, NULL, NULL, BlackCommand }, + { NMAP_QSTOR_TO_COMMAND, NMAP_HELP_NOT_DEFINED, sizeof(NMAP_QSTOR_TO_COMMAND) - 1, CommandQstorTo, NULL, NULL, NULL, BlackCommand }, + { NMAP_QUIT_COMMAND, NMAP_QUIT_HELP, sizeof(NMAP_QUIT_COMMAND) - 1, CommandQuit, NULL, NULL, NULL, BlackCommand }, + { NMAP_QWAIT_COMMAND, NMAP_QWAIT_HELP, sizeof(NMAP_QWAIT_COMMAND) - 1, CommandQwait, NULL, NULL, NULL, BlackCommand }, + { NMAP_QFLUSH_COMMAND, NMAP_QFLUSH_HELP, sizeof(NMAP_QFLUSH_COMMAND) -1, CommandQflush, NULL, NULL, NULL, BlackCommand }, + { NMAP_ADDRESS_RESOLVE_COMMAND, NMAP_ADDRESS_RESOLVE_HELP, sizeof(NMAP_ADDRESS_RESOLVE_COMMAND) -1, CommandAddressResolve, NULL, NULL, NULL, BlackCommand }, + { NMAP_DOMAIN_LOCATION_COMMAND, NMAP_DOMAIN_LOCATION_HELP, sizeof(NMAP_DOMAIN_LOCATION_COMMAND) -1, CommandDomainLocation, NULL, NULL, NULL, BlackCommand }, + { NULL, NULL, 0, NULL, NULL, NULL, NULL, BlackCommand } }; int aliasFindFunc(const void *str, const void *node) { @@ -120,9 +120,9 @@ int hostedFindFunc(const void *str1, const void *str2) { * 2: username == domain * 3: username == auth subsystem */ -BOOL aliasing(char *addr, int *cnt, unsigned char *buffer) { - unsigned char *local = NULL; - unsigned char *domain = NULL; +BOOL aliasing(char *addr, int *cnt, char *buffer) { + char *local = NULL; + char *domain = NULL; int i=-1; BOOL result=FALSE; @@ -239,7 +239,7 @@ BOOL aliasing(char *addr, int *cnt, unsigned char *buffer) { int CommandAddressResolve(void *param) { BOOL handled=FALSE; int cnt=0; - unsigned char buffer[1024]; /* FIXME: is this too big? */ + char buffer[1024]; /* FIXME: is this too big? */ QueueClient *client = (QueueClient *)param; handled = aliasing(client->buffer + 16, &cnt, buffer); @@ -255,8 +255,8 @@ int CommandDomainLocation(void *param) { QueueClient *client = (QueueClient *)param; // FIXME: is this actually used at all with our current aliasing stuff? /* first find the domain in the request */ - unsigned char *domain = client->buffer + 16; - + char *domain = client->buffer + 16; + #if 0 /* now search for it */ int idx = GArrayFindSorted(Conf.hostedDomains, domain, (ArrayCompareFunc)hostedFindFunc); @@ -271,11 +271,11 @@ int CommandDomainLocation(void *param) { return 0; } -int +int CommandAuth(void *param) { - unsigned char *ptr; - unsigned char credential[XPLHASH_MD5_LENGTH]; + char *ptr; + char credential[XPLHASH_MD5_LENGTH]; QueueClient *client = (QueueClient *)param; xpl_hash_context context; @@ -284,7 +284,7 @@ CommandAuth(void *param) return ConnWriteStr(client->conn, MSG3000UNKNOWN); } ptr += 7; - + XplHashNew(&context, XPLHASH_MD5); XplHashWrite(&context, client->authChallenge, strlen(client->authChallenge)); XplHashWrite(&context, Conf.serverHash, NMAP_HASH_SIZE); @@ -304,11 +304,11 @@ CommandAuth(void *param) return(-1); } -int +int CommandPass(void *param) { - unsigned char *ptr; - unsigned char *pass; + char *ptr; + char *pass; int result; struct sockaddr_in nmap; QueueClient *client = (QueueClient *)param; @@ -316,32 +316,33 @@ CommandPass(void *param) ptr = client->buffer + 4; if (*ptr++ == ' ') { - if ( (toupper(ptr[0]) == 'U') - && (toupper(ptr[1]) == 'S') - && (toupper(ptr[2]) == 'E') - && (toupper(ptr[3]) == 'R') - && (ptr[4] == ' ') - && (ptr[5] != '\0') - && (!isspace(ptr[5])) + if ( (toupper(ptr[0]) == 'U') + && (toupper(ptr[1]) == 'S') + && (toupper(ptr[2]) == 'E') + && (toupper(ptr[3]) == 'R') + && (ptr[4] == ' ') + && (ptr[5] != '\0') + && (!isspace(ptr[5])) && ((pass = strchr(ptr + 5, ' ')) != NULL)) { ptr += 5; *pass++ = '\0'; result = MsgAuthVerifyPassword(ptr, pass); - if ((result==0) && (nmap.sin_addr.s_addr == MsgGetHostIPAddress())) { + if ((result == 0) && (MsgAuthGetUserStore(ptr, &nmap) == 0) && + (nmap.sin_addr.s_addr == MsgGetHostIPAddress())) { return(ConnWrite(client->conn, MSG1000OK, sizeof(MSG1000OK) - 1)); } else { return(ConnWrite(client->conn, MSG4120USERLOCKED, sizeof(MSG4120USERLOCKED) - 1)); } Log(LOG_NOTICE, "PASS USER failed for user from host %s", LOGIP(client->conn->socketAddress)); - } else if ( (toupper(ptr[0]) == 'S') - && (toupper(ptr[1]) == 'Y') - && (toupper(ptr[2]) == 'S') - && (toupper(ptr[3]) == 'T') - && (toupper(ptr[4]) == 'E') - && (toupper(ptr[5]) == 'M') + } else if ( (toupper(ptr[0]) == 'S') + && (toupper(ptr[1]) == 'Y') + && (toupper(ptr[2]) == 'S') + && (toupper(ptr[3]) == 'T') + && (toupper(ptr[4]) == 'E') + && (toupper(ptr[5]) == 'M') && (ptr[6] == ' ')) { ptr += 7; @@ -362,25 +363,25 @@ CommandPass(void *param) ConnWrite(client->conn, MSG3010BADARGC, sizeof(MSG3010BADARGC) - 1); client->done = TRUE; - + return(-1); } -static int +static int CommandQuit(void *param) { QueueClient *client = param; - + client->done = TRUE; return -1; } -static int +static int CommandHelp(void *param) { int ccode; - unsigned char *ptr; + char *ptr; QueueClient *client = (QueueClient *)param; ptr = client->buffer + 4; @@ -394,7 +395,7 @@ CommandHelp(void *param) } else { command = ProtocolCommandTreeSearch(&Agent.authCommands, ptr); } - + if (command) { if (command->help) { ccode = ConnWrite(client->conn, command->help, strlen(command->help)); @@ -415,7 +416,7 @@ CommandHelp(void *param) return(ccode); } -void +void QueueClientFree(void *clientp) { QueueClient *client = clientp; @@ -423,11 +424,11 @@ QueueClientFree(void *clientp) MemPrivatePoolReturnEntry(Agent.clientMemPool, client); } -static BOOL +static BOOL CheckTrustedHost(QueueClient *client) { int i = 0; - + if (Conf.trustedHosts) { /* TODO: this can be optimized a little bit */ XplRWReadLockAcquire(&Conf.lock); @@ -441,22 +442,22 @@ int HandleCommand(QueueClient *client) { int ccode = 0; - + while (Agent.agent.state < BONGO_AGENT_STATE_STOPPING && client && client->conn && !client->done) { client->buffer[0] = '\0'; ccode = ConnReadAnswer(client->conn, client->buffer, CONN_BUFSIZE); - if ((ccode != -1) && (ccode < CONN_BUFSIZE)) { + if ((ccode != -1) && (ccode < CONN_BUFSIZE)) { ProtocolCommand *command; if (client->authorized) { command = ProtocolCommandTreeSearch(&Agent.commands, client->buffer); } else { - command = ProtocolCommandTreeSearch(&Agent.authCommands, client->buffer); + command = ProtocolCommandTreeSearch(&Agent.authCommands, client->buffer); } - + if (command) { ccode = command->handler(client); } else { @@ -465,7 +466,7 @@ HandleCommand(QueueClient *client) } else { ccode = ConnWrite(client->conn, MSG3240NOAUTH, sizeof(MSG3240NOAUTH) - 1); } - + Log(LOG_DEBUG, "Handled command from %s", LOGIP(client->conn->socketAddress)); } } else if (ccode == -1) { @@ -477,11 +478,11 @@ HandleCommand(QueueClient *client) } } - return(ccode); + return(ccode); } static int -ProcessClient(void *clientp, Connection *conn) +ProcessClient(void *clientp, Connection *conn) { QueueClient *client = clientp; client->conn = conn; @@ -495,11 +496,11 @@ ProcessClient(void *clientp, Connection *conn) } else { ccode = ConnWriteF(client->conn, "1000 %s %s\r\n", BongoGlobals.hostname, MSG1000READY); } - + if (ccode == -1) { return -1; } - + if ((ccode = ConnFlush(client->conn)) == -1) { return -1; } @@ -507,9 +508,10 @@ ProcessClient(void *clientp, Connection *conn) return HandleCommand(client); } -static void +static void QueueServer(void *ignored) { + UNUSED_PARAMETER(ignored); XplRenameThread(XplGetThreadID(), AGENT_DN " Server"); XplSafeIncrement(Agent.activeThreads); @@ -556,7 +558,7 @@ ServerSocketInit(int port) conn->socketAddress.sin_port = htons(port); conn->socketAddress.sin_addr.s_addr = MsgGetAgentBindIPAddress(); - /* Get root privs back for the bind. It's ok if this fails - + /* Get root privs back for the bind. It's ok if this fails - * the user might not need to be root to bind to the port */ XplSetEffectiveUserId(0); @@ -589,8 +591,10 @@ CheckDiskspace(BongoAgent *agent) unsigned long bytesPerBlock; unsigned long wantFree; + UNUSED_PARAMETER(agent); + bytesPerBlock = XplGetDiskBlocksize(Conf.spoolPath); - freeBlocks = XplGetDiskspaceFree(Conf.spoolPath); + freeBlocks = XplGetDiskspaceFree(Conf.spoolPath); wantFree = (Conf.minimumFree / bytesPerBlock) + 1; if (freeBlocks != 0x7f000000) { @@ -600,7 +604,7 @@ CheckDiskspace(BongoAgent *agent) /* overflow if a volume had more than 16 TB. */ Agent.flags |= QUEUE_AGENT_DISK_SPACE_LOW; - + Log(LOG_ERROR, "Disk space for %s low: have %ld free, want %ld.", Conf.spoolPath, freeBlocks, wantFree); } else { Agent.flags &= ~QUEUE_AGENT_DISK_SPACE_LOW; @@ -623,19 +627,19 @@ CheckLoad(BongoAgent *agent) if (!Conf.loadMonitorDisabled) { current = XplGetServerUtilization(&upTime, &idleTime); - + if (current <= Conf.loadMonitorLow) { if (state) { if (Conf.defaultConcurrentWorkers > Conf.maxConcurrentWorkers) { Conf.maxConcurrentWorkers *= 2; } - + state = 0; } else { if (Conf.defaultConcurrentWorkers > Conf.maxConcurrentWorkers) { Conf.maxConcurrentWorkers *= 2; } - + if (Conf.defaultSequentialWorkers > Conf.maxSequentialWorkers) { Conf.maxSequentialWorkers *= 2; } @@ -653,12 +657,12 @@ CheckLoad(BongoAgent *agent) } Conf.queueCheck = CalculateCheckQueueLimit(Conf.maxConcurrentWorkers, Conf.maxSequentialWorkers); } - } + } } #endif -static void -SignalHandler(int sigtype) +static void +SignalHandler(int sigtype) { BongoAgentHandleSignal(&Agent.agent, sigtype); } @@ -672,13 +676,16 @@ XplServiceMain(int argc, char *argv[]) int ccode; int startupOpts; + UNUSED_PARAMETER(argc); + UNUSED_PARAMETER(argv); + LogStart(); if (XplSetRealUser(MsgGetUnprivilegedUser()) < 0) { Log(LOG_ERROR, "Could not drop to unprivileged user '%s'", MsgGetUnprivilegedUser()); return -1; } - + XplInit(); /* Set the default port */ @@ -691,7 +698,7 @@ XplServiceMain(int argc, char *argv[]) Log(LOG_ERROR, "Initialization failed exiting."); return -1; } - + XplRWLockInit(&Conf.lock); XplSafeWrite(Agent.activeThreads, 0); @@ -733,9 +740,8 @@ XplServiceMain(int argc, char *argv[]) XplStartMainThread(AGENT_NAME, &id, QueueServer, 8192, NULL, ccode); LogShutdown(); - + XplUnloadApp(XplGetThreadID()); - + return 0; } - diff --git a/src/agents/queue/queued.h b/src/agents/queue/queued.h index fe33c9b..21030ae 100644 --- a/src/agents/queue/queued.h +++ b/src/agents/queue/queued.h @@ -46,10 +46,10 @@ typedef struct { BOOL done; /* Buffers */ - unsigned char authChallenge[MAXEMAILNAMESIZE + 1]; - unsigned char buffer[CONN_BUFSIZE + 1]; - unsigned char line[CONN_BUFSIZE + 1]; - unsigned char path[XPL_MAX_PATH + 1]; + char authChallenge[MAXEMAILNAMESIZE + 1]; + char buffer[CONN_BUFSIZE + 1]; + char line[CONN_BUFSIZE + 1]; + char path[XPL_MAX_PATH + 1]; /* Active queue entry */ struct { @@ -60,7 +60,7 @@ typedef struct { unsigned long id; unsigned long target; - unsigned char workQueue[12]; + char workQueue[12]; MIMEReportStruct *report; } entry; @@ -95,7 +95,7 @@ extern QueueAgent Agent; void QueueClientFree(void *clientp); int HandleCommand(QueueClient *client); -BOOL aliasing(char *addr, int *cnt, unsigned char *buffer); +BOOL aliasing(char *addr, int *cnt, char *buffer); #define STACKSPACE_Q (1024*80) diff --git a/src/agents/rules/CMakeLists.txt b/src/agents/rules/CMakeLists.txt index 762ef7c..80576d6 100644 --- a/src/agents/rules/CMakeLists.txt +++ b/src/agents/rules/CMakeLists.txt @@ -9,6 +9,7 @@ target_link_libraries(bongorules bongoutil bongojson bongomsgapi + libbongosieve ) install(TARGETS bongorules DESTINATION ${SBIN_INSTALL_DIR}) diff --git a/src/agents/rules/rules.c b/src/agents/rules/rules.c index f03199e..43c23f4 100644 --- a/src/agents/rules/rules.c +++ b/src/agents/rules/rules.c @@ -30,11 +30,373 @@ #include #include #include +#include #include #include "rules.h" -RulesAgentGlobals RulesAgent = {{0,}, }; +RulesAgentGlobals RulesAgent = {0}; + +typedef struct { + RulesClient *client; + const char *message; + int result; + BOOL written; +} SieveExecution; + +static int SieveAddress(const char *input, char output[1025]); + +static int +SieveAutomatedMessage(const char *message) +{ + const char *value; + if (!message) return 1; + value = strcasestr(message, "\nAuto-Submitted:"); + if (value && strncasecmp(value + 16, " no", 3)) return 1; + if (strcasestr(message, "\nList-Id:") || strcasestr(message, "\nList-Post:") || + strcasestr(message, "\nX-Auto-Response-Suppress:")) return 1; + value = strcasestr(message, "\nPrecedence:"); + if (value && (strcasestr(value, "bulk") == value + 12 || + strcasestr(value, "list") == value + 12 || + strcasestr(value, "junk") == value + 12)) return 1; + return 0; +} + +static void +SieveVacationHandle(const BongoSieveResult *result, char handle[33]) +{ + unsigned long hash = 2166136261UL; + const unsigned char *cursor; + if (result->handle && *result->handle && strlen(result->handle) <= 32) { + strcpy(handle, result->handle); + return; + } + for (cursor = (const unsigned char *)result->argument; *cursor; cursor++) { + hash = (hash ^ *cursor) * 16777619UL; + } + snprintf(handle, 33, "%08lx", hash & 0xffffffffUL); +} + +static int +SieveVacationMimeSafe(const char *text) +{ + const char *line = text; + int contentType = 0; + if (!text || !*text) return 0; + while (*line) { + const char *end = strchr(line, '\n'); + size_t length = end ? (size_t)(end - line) : strlen(line); + if (length && line[length - 1] == '\r') length--; + if (!length) return contentType; + if (length > 998) return 0; + if (*line != ' ' && *line != '\t') { + const char *colon = memchr(line, ':', length); + size_t nameLength; + if (!colon) return 0; + nameLength = (size_t)(colon - line); + if (nameLength == 12 && !strncasecmp(line, "Content-Type", 12)) { + contentType = 1; + } else if (!((nameLength == 25 && !strncasecmp(line, "Content-Transfer-Encoding", 25)) || + (nameLength == 19 && !strncasecmp(line, "Content-Disposition", 19)) || + (nameLength == 16 && !strncasecmp(line, "Content-Language", 16)))) { + return 0; + } + } + if (!end) return 0; + line = end + 1; + } + return 0; +} + +static char * +SieveNormalizeCRLF(const char *input) +{ + size_t length = strlen(input), index, output = 0; + char *normalized = malloc(length * 2 + 3); + if (!normalized) return NULL; + for (index = 0; index < length; index++) { + if (input[index] == '\r') { + if (index + 1 >= length || input[index + 1] != '\n') { + free(normalized); + return NULL; + } + normalized[output++] = '\r'; + normalized[output++] = '\n'; + index++; + } else if (input[index] == '\n') { + normalized[output++] = '\r'; + normalized[output++] = '\n'; + } else { + normalized[output++] = input[index]; + } + } + normalized[output] = '\0'; + return normalized; +} + +static int +SieveSendVacation(SieveExecution *execution, const BongoSieveResult *result) +{ + RulesClient *client = execution->client; + const char *sender = client->sender; + const char *from = result->from ? result->from : client->recipient; + const char *subject = result->subject ? result->subject : "Automatic reply"; + char safeSender[1025], safeFrom[1025], handle[33]; + char safeAlias[1025]; + char *reply, *reason; + size_t replySize; + size_t alias; + unsigned long interval; + int code; + if (!sender || !strcmp(sender, "-") || SieveAutomatedMessage(execution->message) || + !SieveAddress(sender, safeSender) || !SieveAddress(from, safeFrom) || + strcasecmp(safeFrom, client->recipient) || strchr(subject, '\r') || + strchr(subject, '\n') || strlen(subject) > 998 || + (result->mime && !SieveVacationMimeSafe(result->argument))) return 1; + for (alias = 0; alias < result->address_count; alias++) { + if (!SieveAddress(result->addresses[alias], safeAlias)) return 0; + } + reason = SieveNormalizeCRLF(result->argument); + if (!reason) return 0; + interval = result->has_seconds ? result->seconds : result->days * 86400UL; + if (interval < 3600) interval = 3600; + SieveVacationHandle(result, handle); + if (!BongoSieveVacationClaim(RulesAgent.SieveStore, client->recipient, + safeSender, handle, interval)) { free(reason); return 1; } + replySize = strlen(reason) + strlen(safeSender) + strlen(safeFrom) + + strlen(subject) + 320; + reply = malloc(replySize); + if (!reply) { + BongoSieveVacationRelease(RulesAgent.SieveStore, client->recipient, + safeSender, handle); + free(reason); + return 0; + } + code = snprintf(reply, replySize, + "From: %s\r\nTo: %s\r\nSubject: %s\r\n" + "Auto-Submitted: auto-replied\r\nPrecedence: bulk\r\n" + "MIME-Version: 1.0\r\n%s%s%s", + safeFrom, safeSender, subject, + result->mime ? "" : "Content-Type: text/plain; charset=UTF-8\r\n" + "Content-Transfer-Encoding: 8bit\r\n\r\n", + reason, result->mime ? "" : "\r\n"); + free(reason); + if (code < 0 || (size_t)code >= replySize) { + BongoSieveVacationRelease(RulesAgent.SieveStore, client->recipient, + safeSender, handle); + free(reply); + return 0; + } +#define VACATION_COMMAND(command) \ + (ConnWriteF(client->conn, command) != -1 && ConnFlush(client->conn) != -1 && \ + NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, FALSE) == NMAP_OK) + if (!VACATION_COMMAND("QCREA\r\n") || + !VACATION_COMMAND("QSTOR FROM - -\r\n") || + ConnWriteF(client->conn, "QSTOR TO %s - 0\r\n", safeSender) == -1 || + ConnFlush(client->conn) == -1 || + NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, FALSE) != NMAP_OK || + ConnWriteF(client->conn, "QSTOR MESSAGE %d\r\n", code) == -1 || + ConnWrite(client->conn, reply, code) == -1 || ConnFlush(client->conn) == -1 || + NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, FALSE) != NMAP_OK || + !VACATION_COMMAND("QRUN\r\n")) { + ConnWriteF(client->conn, "QABRT\r\n"); + ConnFlush(client->conn); + NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, FALSE); + BongoSieveVacationRelease(RulesAgent.SieveStore, client->recipient, + safeSender, handle); + free(reply); + return 0; + } +#undef VACATION_COMMAND + Log(LOG_INFO, "Sent vacation response for %s to %s", client->recipient, safeSender); + free(reply); + return 1; +} + +static int +SieveSendReject(SieveExecution *execution, const BongoSieveResult *result) +{ + RulesClient *client = execution->client; + const char *sender = client->sender; + char safeSender[1025], safeRecipient[1025], boundary[96]; + char *reason, *notice; + size_t noticeSize; + int code; + + /* RFC 5429 permits suppressing a notification where it would amplify + * forged or automated mail. The rejected message is still discarded. */ + if (!sender || !strcmp(sender, "-") || SieveAutomatedMessage(execution->message) || + !SieveAddress(sender, safeSender)) { + Log(LOG_NOTICE, "Suppressed Sieve reject notification for %s (unsafe sender)", + client->recipient); + execution->written = TRUE; + return 1; + } + if (!SieveAddress(client->recipient, safeRecipient)) return 0; + reason = SieveNormalizeCRLF(result->argument); + if (!reason || strlen(reason) > 64 * 1024 || strstr(reason, "\r\n.\r\n")) { + free(reason); + return 0; + } + snprintf(boundary, sizeof(boundary), "bongo-reject-%lu-%lu", + (unsigned long)getpid(), (unsigned long)time(NULL)); + noticeSize = strlen(reason) + strlen(safeSender) + strlen(safeRecipient) + + strlen(boundary) * 3 + 1024; + notice = malloc(noticeSize); + if (!notice) { free(reason); return 0; } + code = snprintf(notice, noticeSize, + "From: Mail Delivery Subsystem <%s>\r\nTo: %s\r\n" + "Subject: Message rejected by recipient mail filter\r\n" + "Auto-Submitted: auto-replied\r\nMIME-Version: 1.0\r\n" + "Content-Type: multipart/report; report-type=disposition-notification; " + "boundary=\"%s\"\r\n\r\n" + "--%s\r\nContent-Type: text/plain; charset=UTF-8\r\n" + "Content-Transfer-Encoding: 8bit\r\n\r\n" + "The message was refused by the recipient's mail filtering program.\r\n" + "The reason given was:\r\n\r\n%s\r\n" + "--%s\r\nContent-Type: message/disposition-notification\r\n\r\n" + "Reporting-UA: Bongo; %s\r\nFinal-Recipient: rfc822; %s\r\n" + "Disposition: automatic-action/MDN-sent-automatically; deleted\r\n\r\n" + "--%s--\r\n", + safeRecipient, safeSender, boundary, boundary, reason, boundary, + BONGO_VERSION, safeRecipient, boundary); + free(reason); + if (code < 0 || (size_t)code >= noticeSize) { free(notice); return 0; } +#define REJECT_COMMAND(command) \ + (ConnWriteF(client->conn, command) != -1 && ConnFlush(client->conn) != -1 && \ + NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, FALSE) == NMAP_OK) + if (!REJECT_COMMAND("QCREA\r\n") || + !REJECT_COMMAND("QSTOR FROM - -\r\n") || + ConnWriteF(client->conn, "QSTOR TO %s - 0\r\n", safeSender) == -1 || + ConnFlush(client->conn) == -1 || + NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, FALSE) != NMAP_OK || + ConnWriteF(client->conn, "QSTOR MESSAGE %d\r\n", code) == -1 || + ConnWrite(client->conn, notice, code) == -1 || ConnFlush(client->conn) == -1 || + NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, FALSE) != NMAP_OK || + !REJECT_COMMAND("QRUN\r\n")) { + ConnWriteF(client->conn, "QABRT\r\n"); + ConnFlush(client->conn); + NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, FALSE); + free(notice); + return 0; + } +#undef REJECT_COMMAND + Log(LOG_INFO, "Sent Sieve reject notification for %s to %s", + client->recipient, safeSender); + free(notice); + execution->written = TRUE; + return 1; +} + +static int +SieveMailboxName(const char *input, char output[1025]) +{ + size_t index, length; + if (!input || !(length = strlen(input)) || length > 1024) return 0; + for (index = 0; index < length; index++) { + unsigned char value = (unsigned char)input[index]; + if (value < 0x20 || value == 0x7f) return 0; + output[index] = value == ' ' ? 0x7f : (char)value; + } + output[length] = '\0'; + return 1; +} + +static int +SieveAddress(const char *input, char output[1025]) +{ + char *local = NULL, *domain = NULL; + size_t length; + int valid; + if (!input || !(length = strlen(input)) || length > 1024 || + strchr(input, '\r') || strchr(input, '\n') || strchr(input, ' ')) return 0; + memcpy(output, input, length + 1); + valid = MsgParseAddress(output, length, &local, &domain) && local && *local && + domain && *domain; + MemFree(local); + MemFree(domain); + return valid; +} + +static int +SieveAction(const BongoSieveResult *action_result, void *data) +{ + SieveExecution *execution = data; + RulesClient *client = execution->client; + BongoSieveAction action = action_result->action; + const char *argument = action_result->argument; + char safe[1025]; + if (execution->result == -1) return 0; + switch (action) { + case BONGO_SIEVE_KEEP: + execution->result = ConnWriteF(client->conn, "QMOD RAW %s\r\n", + client->envelopeLine); + execution->written = TRUE; + break; + case BONGO_SIEVE_DISCARD: + execution->written = TRUE; + break; + case BONGO_SIEVE_FILEINTO: + if (!SieveMailboxName(argument, safe)) return 0; + execution->result = NMAPSendCommandF(client->conn, + "QMOD MAILBOX %s %s %lu %s\r\n", client->recipient, + client->recipient, (unsigned long)(client->flags | NO_RULEPROCESSING), + safe); + execution->written = TRUE; + break; + case BONGO_SIEVE_REDIRECT: + if (!SieveAddress(argument, safe)) return 0; + execution->result = NMAPSendCommandF(client->conn, + "QMOD TO %s %s %lu\r\n", safe, client->recipient, + (unsigned long)((client->flags & ~(DSN_SUCCESS | DSN_TIMEOUT | + DSN_FAILURE | DSN_HEADER | DSN_BODY)) | NO_RULEPROCESSING)); + execution->written = TRUE; + break; + case BONGO_SIEVE_REJECT: + return SieveSendReject(execution, action_result); + case BONGO_SIEVE_VACATION: + return SieveSendVacation(execution, action_result); + } + return execution->result != -1; +} + +static int +ProcessSieve(RulesClient *client) +{ + char *script = NULL; + char *message = NULL; + size_t script_length = 0; + int active = 0; + long message_length; + SieveExecution execution = { client, NULL, 0, FALSE }; + if (!RulesAgent.SieveStore || + !BongoSieveStoreGet(RulesAgent.SieveStore, client->recipient, NULL, + &script, &script_length, &active) || !active) return 0; + if (NMAPSendCommandF(client->conn, "QRETR %s MESSAGE\r\n", client->qID) == -1 || + NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, TRUE) != 2023) goto failed; + message_length = strtol(client->line, NULL, 10); + if (message_length <= 0 || message_length > 100 * 1024 * 1024) goto failed; + message = malloc((size_t)message_length + 1); + if (!message || ConnReadCount(client->conn, message, (int)message_length) != message_length || + NMAPReadAnswer(client->conn, client->line, CONN_BUFSIZE, TRUE) != NMAP_OK) goto failed; + message[message_length] = '\0'; + execution.message = message; + if (!BongoSieveEvaluate(script, script_length, message, (size_t)message_length, + SieveAction, &execution) || execution.result == -1) goto failed; + if (!execution.written) { + execution.result = ConnWriteF(client->conn, "QMOD RAW %s\r\n", + client->envelopeLine); + } + free(message); + free(script); + return execution.result == -1 ? -1 : 1; +failed: + Log(LOG_ERROR, "Sieve processing failed for user %s queue %s; keeping message", + client->recipient, client->qID); + free(message); + free(script); + return ConnWriteF(client->conn, "QMOD RAW %s\r\n", client->envelopeLine) == -1 ? -1 : 1; +} int GetMimeStructure(RulesClient *c) @@ -56,9 +418,9 @@ GetMimeStructure(RulesClient *c) } void -GetMimeSizes(unsigned char *p, unsigned long *s, unsigned long *l, unsigned long *h) +GetMimeSizes(char *p, unsigned long *s, unsigned long *l, unsigned long *h) { - unsigned char *eof = strrchr(p, '\"'); + char *eof = strrchr(p, '\"'); if (eof) { eof++; sscanf(eof, " %lu %lu %lu", s, l, h); @@ -137,20 +499,20 @@ CompareRuleID(const void *first, const void *second) { unsigned long oneID; unsigned long twoID; - unsigned char temp; - const unsigned char *one = *(unsigned char **)first; - const unsigned char *two = *(unsigned char **)second; + char temp; + char *const one = *(char *const *)first; + char *const two = *(char *const *)second; - if (one && two && (strlen((unsigned char *)one) > 9) && (strlen((unsigned char *)two) > 9)) { - temp = ((unsigned char *)one)[8]; - ((unsigned char *)one)[8] = '\0'; - oneID = strtol((unsigned char *)one, NULL, 16); - ((unsigned char *)one)[8] = temp; + if (one && two && (strlen(one) > 9) && (strlen(two) > 9)) { + temp = one[8]; + one[8] = '\0'; + oneID = strtoul(one, NULL, 16); + one[8] = temp; - temp = ((unsigned char *)two)[8]; - ((unsigned char *)two)[8] = '\0'; - twoID = strtol((unsigned char *)two, NULL, 16); - ((unsigned char *)two)[8] = temp; + temp = two[8]; + two[8] = '\0'; + twoID = strtoul(two, NULL, 16); + two[8] = temp; if (oneID < twoID) { return(-1); @@ -170,8 +532,7 @@ ProcessRules(RulesClient *client) unsigned long start; unsigned long size; unsigned long header; - unsigned char *ptr; - unsigned char *owner; + char *ptr; BOOL result, written=FALSE; BongoRuleAction *action; BongoRuleCondition *condition; @@ -274,6 +635,43 @@ ProcessRules(RulesClient *client) break; } + case RULE_COND_MAILINGLIST: { + static const struct { + const char *header; + const char *value; + } signals[] = { + { "List-Id", "<" }, + { "List-Post", "mailto:" }, + { "Precedence", "list" } + }; + unsigned int signal; + for (signal = 0; signal < sizeof(signals) / sizeof(signals[0]); signal++) { + ccode = NMAPSendCommandF(client->conn, + "QSRCH HEADER %s %s: \"%s\"\r\n", + client->qID, signals[signal].header, + signals[signal].value); + if (ccode != -1 && + (ccode = NMAPReadAnswer(client->conn, client->line, + CONN_BUFSIZE, TRUE)) == NMAP_OK) { + result = TRUE; + break; + } + } + break; + } + + case RULE_COND_LISTID: { + ccode = NMAPSendCommandF(client->conn, + "QSRCH HEADER %s List-Id: \"%s\"\r\n", + client->qID, condition->string[0]); + if (ccode != -1 && + (ccode = NMAPReadAnswer(client->conn, client->line, + CONN_BUFSIZE, TRUE)) == NMAP_OK) { + result = TRUE; + } + break; + } + case RULE_COND_BODY: case RULE_COND_BODY_NOT: { GetMimeStructure(client); @@ -455,7 +853,6 @@ ProcessRules(RulesClient *client) } case RULE_ACT_MOVE: { - owner = NULL; ptr = NULL; used = 0; @@ -471,16 +868,6 @@ ProcessRules(RulesClient *client) ccode = NMAPSendCommandF(client->conn, "QMOD MAILBOX %s %s %lu %s\r\n", client->recipient, client->recipient, (unsigned long)client->flags, action->string[0]); written = TRUE; - if (owner) { - MemFree(owner); - owner = NULL; - } - - if (ptr) { - MemFree(ptr); - ptr = NULL; - } - break; } @@ -608,9 +995,9 @@ ProcessRules(RulesClient *client) static void ParseRules(RulesClient *client) { - unsigned char *cur; - unsigned char *ptr; - unsigned char *limit; + char *cur; + char *ptr; + char *limit; unsigned long used; BOOL parsed = FALSE; BongoRuleAction *action = NULL; @@ -621,7 +1008,7 @@ ParseRules(RulesClient *client) g_array_sort(client->RulesStrings, CompareRuleID); for (used = 0; used < client->RulesStrings->len; used++) { - cur = g_array_index(client->RulesStrings, unsigned char *, used); + cur = g_array_index(client->RulesStrings, char *, used); ptr = cur; limit = cur + strlen(cur); @@ -670,7 +1057,8 @@ ParseRules(RulesClient *client) switch (*ptr) { case RULE_COND_ANY: case RULE_COND_FREE: - case RULE_COND_FREENOT: { + case RULE_COND_FREENOT: + case RULE_COND_MAILINGLIST: { /* no arguments */ if (memcmp(ptr + 1, "000Z000Z", 8) == 0) { condition->type = *ptr; @@ -697,7 +1085,8 @@ ParseRules(RulesClient *client) case RULE_COND_PRIORITY_NOT: case RULE_COND_BODY_NOT: case RULE_COND_HASMIMETYPE: - case RULE_COND_HASMIMETYPENOT: { + case RULE_COND_HASMIMETYPENOT: + case RULE_COND_LISTID: { /* sole argument is a search string */ condition->type = *ptr; condition->length[0] = ((ptr[1] - '0') * 100) + ((ptr[2] - '0') * 10) + (ptr[3] - '0'); @@ -991,7 +1380,8 @@ ParseRules(RulesClient *client) continue; } - Log(LOG_ERROR, "User %s rule %lu cannot be parsed! Offset %lu", client->recipient, used, cur); + Log(LOG_ERROR, "User %s rule %lu cannot be parsed! Offset %lu", + client->recipient, used, (unsigned long)(ptr - cur)); if (condition) { MemFree(condition); @@ -1099,7 +1489,7 @@ ProcessEntry(void *clientp, Connection *conn) envelopeLine = Queue->envelope; while (*envelopeLine) { - unsigned char *eol = envelopeLine + strlen(envelopeLine); + char *eol = envelopeLine + strlen(envelopeLine); switch(*envelopeLine) { case QUEUE_FROM : @@ -1136,6 +1526,10 @@ ProcessEntry(void *clientp, Connection *conn) Queue->recipient = envelopeLine; } + /* An active Sieve script replaces the legacy per-user rule list. */ + ccode = ProcessSieve(Queue); + if (ccode != 0) break; + /* i need to read out this user's rules from the server and parse them */ snprintf(Queue->line, CONN_BUFSIZE, "rules/%s", Queue->recipient); if (!ReadBongoConfiguration(Queue->UserConfig, Queue->line)) { @@ -1174,6 +1568,8 @@ RulesAgentServer(void *ignored) int maxThreads; int minSleep; + UNUSED_PARAMETER(ignored); + XplRenameThread(XplGetThreadID(), AGENT_DN " Server"); BongoQueueAgentGetThreadPoolParameters(&RulesAgent.agent, &minThreads, &maxThreads, &minSleep); @@ -1227,6 +1623,9 @@ XplServiceMain(int argc, char *argv[]) int startupOpts; ConnSSLConfiguration sslconfig; + UNUSED_PARAMETER(argc); + UNUSED_PARAMETER(argv); + if (XplSetRealUser(MsgGetUnprivilegedUser()) < 0) { XplConsolePrintf(AGENT_NAME ": Could not drop to unprivileged user '%s'\r\n" AGENT_NAME ": exiting.\n", MsgGetUnprivilegedUser()); return -1; @@ -1250,10 +1649,21 @@ XplServiceMain(int argc, char *argv[]) sslconfig.key.type = GNUTLS_X509_FMT_PEM; RulesAgent.SSL_Context = ConnSSLContextAlloc(&sslconfig); + { + char sieveDatabase[XPL_MAX_PATH + 1]; + snprintf(sieveDatabase, sizeof(sieveDatabase), "%s/sieve.sqlite", + XPL_DEFAULT_DBF_DIR); + if (!BongoSieveStoreOpen(&RulesAgent.SieveStore, sieveDatabase)) { + XplConsolePrintf(AGENT_NAME ": Cannot open Sieve script store\r\n"); + return -1; + } + } + XplSignalHandler(SignalHandler); /* Start the server thread */ XplStartMainThread(AGENT_NAME, &id, RulesAgentServer, 8192, NULL, ccode); + BongoSieveStoreClose(&RulesAgent.SieveStore); XplUnloadApp(XplGetThreadID()); diff --git a/src/agents/rules/rules.h b/src/agents/rules/rules.h index b72b3e0..272b995 100644 --- a/src/agents/rules/rules.h +++ b/src/agents/rules/rules.h @@ -30,6 +30,7 @@ #include #include #include +#include #define AGENT_NAME "bongorules" @@ -39,7 +40,7 @@ typedef struct _BongoRuleCondition { unsigned char type; unsigned char operand; - unsigned char *string[2]; + char *string[2]; unsigned long length[2]; unsigned long count[2]; @@ -50,7 +51,7 @@ typedef struct _BongoRuleAction { struct _BongoRuleAction *next; unsigned char type; - unsigned char *string[2]; + char *string[2]; unsigned long length[2]; } BongoRuleAction; @@ -59,8 +60,8 @@ typedef struct _BongoRule { struct _BongoRule *prev; struct _BongoRule *next; - unsigned char *string; - unsigned char id[9]; + char *string; + char id[9]; BOOL active; @@ -78,9 +79,9 @@ typedef struct _BongoRule { typedef struct { Connection *conn; - unsigned int envelopeLength; - unsigned int messageLength; - unsigned int envelopeLines; + int envelopeLength; + int messageLength; + int envelopeLines; char *envelope; char *sender; /* pointer into the envelope of the from address */ char *authSender; /* pointer into the envelope of the envelope from */ @@ -115,6 +116,7 @@ typedef struct _RulesAgentGlobals { void *OutgoingPool; BongoThreadPool *OutgoingThreadPool; bongo_ssl_context *SSL_Context; + BongoSieveStore *SieveStore; } RulesAgentGlobals; extern RulesAgentGlobals RulesAgent; @@ -128,5 +130,5 @@ static void FreeBongoRule(BongoRule *rule); static int ProcessRules(RulesClient *client); int GetMimeStructure(RulesClient *c); static void RulesCleanupInformation(RulesClient *client); -void GetMimeSizes(unsigned char *p, unsigned long *s, unsigned long *l, unsigned long *h); +void GetMimeSizes(char *p, unsigned long *s, unsigned long *l, unsigned long *h); #endif diff --git a/src/agents/sieve/CMakeLists.txt b/src/agents/sieve/CMakeLists.txt new file mode 100644 index 0000000..8e1d30b --- /dev/null +++ b/src/agents/sieve/CMakeLists.txt @@ -0,0 +1,5 @@ +StrictCompile() +add_executable(bongosieve managesieved.c) +target_link_libraries(bongosieve libbongosieve bongosasl bongoxpl bongoconnio + bongoutil bongomsgapi bongostreamio) +install(TARGETS bongosieve DESTINATION ${SBIN_INSTALL_DIR}) diff --git a/src/agents/sieve/managesieved.c b/src/agents/sieve/managesieved.c new file mode 100644 index 0000000..219e207 --- /dev/null +++ b/src/agents/sieve/managesieved.c @@ -0,0 +1,326 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define AGENT_NAME "bongosieve" +#define LINE_SIZE 8192 + +static BongoAgent agent; +static Connection *listener; +static bongo_ssl_context *tls_context; +static BongoSieveStore *script_store; +static int server_port = 4190; +static int maximum_script_size = 1024 * 1024; +static int maximum_connections = 64; +static BOOL allow_legacy_tls = FALSE; +static char *listen_address; +static XplThreadID server_id; +static XplAtomic active_connections; + +static BongoConfigItem SieveConfig[] = { + { BONGO_JSON_STRING, "o:listen_address/s", &listen_address }, + { BONGO_JSON_INT, "o:port/i", &server_port }, + { BONGO_JSON_BOOL, "o:allow_legacy_tls/b", &allow_legacy_tls }, + { BONGO_JSON_INT, "o:maximum_script_size/i", &maximum_script_size }, + { BONGO_JSON_INT, "o:maximum_connections/i", &maximum_connections }, + { BONGO_JSON_NULL, NULL, NULL } +}; + +static int write_ok(Connection *connection, const char *message) +{ + return message ? ConnWriteF(connection, "OK \"%s\"\r\n", message) + : ConnWriteStr(connection, "OK\r\n"); +} + +static int write_no(Connection *connection, const char *code, const char *message) +{ + return ConnWriteF(connection, "NO (%s) \"%s\"\r\n", code, message); +} + +static int capabilities(Connection *connection, int tls, const char *mechanisms) +{ + if (ConnWriteF(connection, "\"IMPLEMENTATION\" \"Bongo %s\"\r\n", + BONGO_VERSION) == -1 || + ConnWriteStr(connection, + "\"SIEVE\" \"fileinto envelope body variables vacation vacation-seconds\"\r\n" + "\"MAXREDIRECTS\" \"4\"\r\n") == -1) return -1; + if (!tls && ConnWriteStr(connection, "\"STARTTLS\"\r\n") == -1) return -1; + if (tls && mechanisms && ConnWriteF(connection, "\"SASL\" \"%s\"\r\n", + mechanisms) == -1) return -1; + return ConnWriteStr(connection, "OK\r\n"); +} + +static int verify_password(void *context, const char *user, const char *password, + size_t password_length) +{ + char *copy; + int valid; + (void)context; + if (!user || !password || password_length > 4096) return 0; + copy = malloc(password_length + 1); + if (!copy) return 0; + memcpy(copy, password, password_length); + copy[password_length] = '\0'; + valid = MsgAuthFindUser(user) == 0 && MsgAuthVerifyPassword(user, copy) == 0; + memset(copy, 0, password_length); + free(copy); + return valid; +} + +static int read_literal(Connection *connection, size_t length, char **output) +{ + size_t used = 0; + int count; + char *buffer; + if (!length || length > (size_t)maximum_script_size) return 0; + buffer = malloc(length + 1); + if (!buffer) return 0; + while (used < length) { + count = ConnRead(connection, buffer + used, (int)(length - used)); + if (count <= 0) { free(buffer); return 0; } + used += (size_t)count; + } + buffer[length] = '\0'; + *output = buffer; + return 1; +} + +struct list_context { Connection *connection; int failed; }; +static int list_script(const char *name, int active, void *data) +{ + struct list_context *context = data; + if (ConnWriteF(context->connection, "\"%s\"%s\r\n", name, + active ? " ACTIVE" : "") == -1) { + context->failed = 1; + return 0; + } + return 1; +} + +static int authenticate(Connection *connection, BongoSaslSession *sasl, + BongoManageSieveRequest *request, char **username) +{ + unsigned char decoded[8192]; + char encoded[16384]; + size_t decoded_length = 0, encoded_length = 0, output_length = 0; + const void *output = NULL; + int result; + if (request->argument_count == 2 && + !BongoSaslDecode64(request->arguments[1], strlen(request->arguments[1]), + decoded, sizeof(decoded), &decoded_length)) return 0; + result = BongoSaslStart(sasl, request->arguments[0], + request->argument_count == 2 ? decoded : NULL, + decoded_length, &output, &output_length); + while (result == BONGO_SASL_CONTINUE) { + if (!BongoSaslEncode64(output, output_length, encoded, sizeof(encoded), + &encoded_length) || + ConnWriteF(connection, "\"%.*s\"\r\n", (int)encoded_length, encoded) == -1 || + ConnFlush(connection) == -1 || + ConnReadLine(connection, encoded, sizeof(encoded)) <= 0) return 0; + CHOP_NEWLINE(encoded); + if (!strcmp(encoded, "*")) return 0; + /* RFC 5804 SASL responses are strings, so clients commonly quote the + * Base64 value. Base64 itself cannot contain a quote or backslash. */ + if (encoded[0] == '\"') { + size_t length = strlen(encoded); + if (length < 2 || encoded[length - 1] != '\"') return 0; + memmove(encoded, encoded + 1, length - 2); + encoded[length - 2] = '\0'; + } + if (!BongoSaslDecode64(encoded, strlen(encoded), decoded, sizeof(decoded), + &decoded_length)) return 0; + result = BongoSaslStep(sasl, decoded, decoded_length, &output, &output_length); + } + memset(decoded, 0, sizeof(decoded)); + if (result != BONGO_SASL_COMPLETE || !BongoSaslUsername(sasl)) return 0; + *username = strdup(BongoSaslUsername(sasl)); + return *username != NULL; +} + +static void serve(Connection *connection) +{ + char line[LINE_SIZE + 1]; + char *user = NULL; + BongoSaslSession *sasl = NULL; + int tls = 0; + const char *mechanisms = NULL; + BongoManageSieveRequest request; + capabilities(connection, 0, NULL); + ConnFlush(connection); + while (ConnReadLine(connection, line, sizeof(line)) > 0) { + char *literal = NULL; + if (!BongoManageSieveParse(line, strlen(line), &request)) { + write_no(connection, "SYNTAX", "Invalid command"); + ConnFlush(connection); + continue; + } + if (request.literal_length) { + if (!request.literal_nonsynchronizing) { + ConnWriteStr(connection, "OK \"Ready for literal data\"\r\n"); + ConnFlush(connection); + } + if (!read_literal(connection, request.literal_length, &literal)) { + write_no(connection, "QUOTA/MAXSIZE", "Script is too large"); + ConnFlush(connection); + continue; + } + } + if (request.command == BONGO_MANAGESIEVE_CAPABILITY) { + capabilities(connection, tls, mechanisms); + } else if (request.command == BONGO_MANAGESIEVE_STARTTLS && !tls && !user) { + ConnWriteStr(connection, "OK \"Begin TLS negotiation\"\r\n"); + ConnFlush(connection); + if (!ConnNegotiate(connection, tls_context) || + !BongoSaslSessionCreate(&sasl, "sieve", NULL, NULL, NULL, 1, + verify_password, NULL) || + !BongoSaslMechanisms(sasl, &mechanisms)) break; + tls = 1; + } else if (request.command == BONGO_MANAGESIEVE_AUTHENTICATE && tls && !user) { + if (authenticate(connection, sasl, &request, &user)) write_ok(connection, NULL); + else write_no(connection, "AUTH-FAILED", "Authentication failed"); + } else if (request.command == BONGO_MANAGESIEVE_LOGOUT) { + write_ok(connection, "Logout complete"); + ConnFlush(connection); + free(literal); + break; + } else if (!user) { + write_no(connection, "AUTH-REQUIRED", "Authenticate first"); + } else if (request.command == BONGO_MANAGESIEVE_CHECKSCRIPT) { + if (BongoSieveValidate(literal, request.literal_length)) write_ok(connection, NULL); + else write_no(connection, "QUOTA", "Script validation failed"); + } else if (request.command == BONGO_MANAGESIEVE_HAVESPACE) { + write_ok(connection, NULL); + } else if (request.command == BONGO_MANAGESIEVE_PUTSCRIPT) { + if (BongoSieveStorePut(script_store, user, request.arguments[0], literal, + request.literal_length)) write_ok(connection, NULL); + else write_no(connection, "QUOTA", "Script could not be stored"); + } else if (request.command == BONGO_MANAGESIEVE_SETACTIVE) { + if (BongoSieveStoreSetActive(script_store, user, request.arguments[0])) write_ok(connection, NULL); + else write_no(connection, "NONEXISTENT", "Script does not exist"); + } else if (request.command == BONGO_MANAGESIEVE_DELETESCRIPT) { + if (BongoSieveStoreDelete(script_store, user, request.arguments[0])) write_ok(connection, NULL); + else write_no(connection, "ACTIVE", "Active or nonexistent script"); + } else if (request.command == BONGO_MANAGESIEVE_RENAMESCRIPT) { + if (BongoSieveStoreRename(script_store, user, request.arguments[0], + request.arguments[1])) write_ok(connection, NULL); + else write_no(connection, "ALREADYEXISTS", "Source missing or destination exists"); + } else if (request.command == BONGO_MANAGESIEVE_LISTSCRIPTS) { + struct list_context context = { connection, 0 }; + if (BongoSieveStoreList(script_store, user, list_script, &context) && !context.failed) + write_ok(connection, NULL); + } else if (request.command == BONGO_MANAGESIEVE_GETSCRIPT) { + char *script = NULL; size_t length = 0; + if (BongoSieveStoreGet(script_store, user, request.arguments[0], &script, &length, NULL)) { + ConnWriteF(connection, "{%lu}\r\n", (unsigned long)length); + ConnWrite(connection, script, (int)length); + ConnWriteStr(connection, "\r\nOK\r\n"); + free(script); + } else write_no(connection, "NONEXISTENT", "Script does not exist"); + } else if (request.command == BONGO_MANAGESIEVE_NOOP) { + write_ok(connection, NULL); + } else { + write_no(connection, "UNSUPPORTED", "Command not supported"); + } + free(literal); + if (ConnFlush(connection) == -1) break; + } + free(user); + BongoSaslSessionDestroy(&sasl); +} + +static void +client_loop(void *data) +{ + Connection *connection = data; + serve(connection); + ConnClose(connection); + ConnFree(connection); + XplSafeDecrement(active_connections); +} + +static void server_loop(void *unused) +{ + (void)unused; + while (agent.state < BONGO_AGENT_STATE_STOPPING) { + Connection *connection = NULL; + XplThreadID client_id; + int result; + if (ConnAccept(listener, &connection) == -1) continue; + if (XplSafeRead(active_connections) >= maximum_connections) { + ConnWriteStr(connection, "BYE (TRYLATER) \"Too many connections\"\r\n"); + ConnFlush(connection); + ConnClose(connection); + ConnFree(connection); + continue; + } + XplBeginCountedThread(&client_id, client_loop, 32768, connection, + result, active_connections); + if (result != 0) { + ConnClose(connection); + ConnFree(connection); + } + } +} + +static void signal_handler(int signal) { + BongoAgentHandleSignal(&agent, signal); + if (listener) ConnClose(listener); +} +XplServiceCode(signal_handler) + +int XplServiceMain(int argc, char **argv) +{ + ConnSSLConfiguration tls = {0}; + char database[XPL_MAX_PATH + 1]; + int result; + int wait; + (void)argc; (void)argv; (void)GlobalConfig; + if (XplSetRealUser(MsgGetUnprivilegedUser()) < 0) { + XplConsolePrintf(AGENT_NAME ": Could not drop privileges\r\n"); + return 1; + } + XplInit(); + result = BongoAgentInit(&agent, AGENT_NAME, DEFAULT_CONNECTION_TIMEOUT, + BA_STARTUP_CONNIO | BA_STARTUP_NMAP); + if (result == -1 || !ReadBongoConfiguration(SieveConfig, "sieve") || + server_port < 1 || server_port > 65535 || maximum_script_size < 1 || + maximum_script_size > 1024 * 1024 || maximum_connections < 1 || + maximum_connections > 4096 || !BongoSaslInitialize()) return 1; + snprintf(database, sizeof(database), "%s/sieve.sqlite", XPL_DEFAULT_DBF_DIR); + if (!BongoSieveStoreOpen(&script_store, database)) return 1; + tls.certificate.file = XPL_DEFAULT_CERT_PATH; + tls.certificate.type = GNUTLS_X509_FMT_PEM; + tls.key.file = XPL_DEFAULT_KEY_PATH; + tls.key.type = GNUTLS_X509_FMT_PEM; + if (allow_legacy_tls) tls.options |= SSL_ALLOW_LEGACY_PROTOCOLS; + tls_context = ConnSSLContextAlloc(&tls); + listener = ConnAlloc(TRUE); + if (!tls_context || !listener || !listen_address || + inet_aton(listen_address, &listener->socketAddress.sin_addr) == 0) return 1; + listener->socketAddress.sin_family = AF_INET; + listener->socketAddress.sin_port = htons((unsigned short)server_port); + if (ConnServerSocket(listener, 64) == -1) return 1; + XplSignalHandler(signal_handler); + /* Some XPL backends expand this call to a macro which ignores the ID. */ + (void)server_id; + XplStartMainThread(AGENT_NAME, &server_id, server_loop, 32768, NULL, result); + for (wait = 0; wait < 300 && XplSafeRead(active_connections) > 0; wait++) { + XplDelay(100); + } + if (XplSafeRead(active_connections) > 0) { + Log(LOG_WARNING, "%d ManageSieve connections remain during shutdown", + XplSafeRead(active_connections)); + return 1; + } + BongoSieveStoreClose(&script_store); + ConnSSLContextFree(tls_context); + BongoSaslShutdown(); + return 0; +} diff --git a/src/agents/smtp/CMakeLists.txt b/src/agents/smtp/CMakeLists.txt index 54bb18c..305b97e 100644 --- a/src/agents/smtp/CMakeLists.txt +++ b/src/agents/smtp/CMakeLists.txt @@ -10,6 +10,7 @@ target_link_libraries(bongosmtp bongoutil bongojson bongomsgapi + bongomailauth ) add_executable(bongosmtpc @@ -22,6 +23,7 @@ target_link_libraries(bongosmtpc bongoutil bongojson bongomsgapi + bongomailauth ) install(TARGETS bongosmtp DESTINATION ${SBIN_INSTALL_DIR}) diff --git a/src/agents/smtp/smtpc.c b/src/agents/smtp/smtpc.c index 525960d..2ec8dc2 100755 --- a/src/agents/smtp/smtpc.c +++ b/src/agents/smtp/smtpc.c @@ -32,10 +32,174 @@ #include #include #include +#include "mailauth.h" #include "smtpc.h" -SMTPAgentGlobals SMTPAgent = {{0,}, }; +static struct { + BOOL dkim_sign_outgoing; + char *dkim_key_directory; + char *dkim_selector; + char *dkim_signing_domain; + BOOL srs_forward; + char *srs_domain; + char *srs_secret_file; + GArray *lmtp_transports; +} MailAuth; + +static BongoConfigItem LMTPTransportList = { + BONGO_JSON_STRING, NULL, &MailAuth.lmtp_transports +}; + +static BongoConfigItem MailAuthConfig[] = { + { BONGO_JSON_BOOL, "o:dkim_sign_outgoing/b", &MailAuth.dkim_sign_outgoing }, + { BONGO_JSON_STRING, "o:dkim_key_directory/s", &MailAuth.dkim_key_directory }, + { BONGO_JSON_STRING, "o:dkim_selector/s", &MailAuth.dkim_selector }, + { BONGO_JSON_STRING, "o:dkim_signing_domain/s", &MailAuth.dkim_signing_domain }, + { BONGO_JSON_BOOL, "o:srs_forward/b", &MailAuth.srs_forward }, + { BONGO_JSON_STRING, "o:srs_domain/s", &MailAuth.srs_domain }, + { BONGO_JSON_STRING, "o:srs_secret_file/s", &MailAuth.srs_secret_file }, + { BONGO_JSON_ARRAY, "o:lmtp_transports/a", &LMTPTransportList }, + { BONGO_JSON_NULL, NULL, NULL } +}; + +SMTPAgentGlobals SMTPAgent = {0}; + +static BongoMailAuthStatus +GetEnvelopeSender(const SMTPClient *queue, char *rewritten, size_t rewritten_size, + const char **sender) +{ + BOOL forwarded; + + if (queue == NULL || sender == NULL) { + return BONGO_MAILAUTH_INVALID_ARGUMENT; + } + *sender = queue->sender; + forwarded = (queue->flags & MSG_FLAG_SOURCE_EXTERNAL) != 0 && + queue->authSender != NULL && queue->authSender[0] == '-' && + queue->sender != NULL && queue->sender[0] != '\0' && + strcmp(queue->sender, "-") != 0; + if (!MailAuth.srs_forward || !forwarded) { + return BONGO_MAILAUTH_OK; + } + if (MailAuth.srs_domain == NULL || MailAuth.srs_domain[0] == '\0' || + MailAuth.srs_secret_file == NULL || MailAuth.srs_secret_file[0] == '\0') { + return BONGO_MAILAUTH_INVALID_ARGUMENT; + } + if (BongoMailAuthSRSForward(MailAuth.srs_secret_file, + MailAuth.srs_domain, queue->sender, + rewritten, rewritten_size) != BONGO_MAILAUTH_OK) { + return BONGO_MAILAUTH_PROTOCOL_ERROR; + } + *sender = rewritten; + return BONGO_MAILAUTH_OK; +} + +static BOOL +IsDKIMName(const char *value) +{ + const unsigned char *cursor = (const unsigned char *) value; + + if (cursor == NULL || *cursor == '\0') { + return FALSE; + } + while (*cursor != '\0') { + if (!isalnum(*cursor) && *cursor != '.' && *cursor != '-') { + return FALSE; + } + cursor++; + } + return TRUE; +} + +static BongoMailAuthStatus +GenerateDKIMSignature(SMTPClient *queue) +{ + const char *domain; + const char *at; + char keyPath[XPL_MAX_PATH]; + char *messageLine = NULL; + unsigned long messageLineSize = 0; + long long remaining; + BongoDKIMSigner *signer = NULL; + BongoMailAuthStatus status; + + queue->hasDKIMSignature = FALSE; + if (!MailAuth.dkim_sign_outgoing) { + return BONGO_MAILAUTH_OK; + } + /* Do not add a new author signature to unauthenticated forwarded mail. */ + if ((queue->flags & MSG_FLAG_SOURCE_EXTERNAL) != 0 && + (queue->authSender == NULL || queue->authSender[0] == '-')) { + return BONGO_MAILAUTH_OK; + } + domain = MailAuth.dkim_signing_domain; + if (domain == NULL || domain[0] == '\0') { + at = queue->sender != NULL ? strrchr(queue->sender, '@') : NULL; + domain = at != NULL ? at + 1 : NULL; + } + if (!IsDKIMName(domain) || !IsDKIMName(MailAuth.dkim_selector) || + MailAuth.dkim_key_directory == NULL || + MailAuth.dkim_key_directory[0] == '\0') { + return BONGO_MAILAUTH_INVALID_ARGUMENT; + } + if (snprintf(keyPath, sizeof(keyPath), "%s/%s/%s.private", + MailAuth.dkim_key_directory, domain, + MailAuth.dkim_selector) >= (int) sizeof(keyPath)) { + return BONGO_MAILAUTH_INVALID_ARGUMENT; + } + status = BongoMailAuthDKIMSignerCreate( + keyPath, MailAuth.dkim_selector, domain, queue->qID, &signer); + if (status != BONGO_MAILAUTH_OK) { + return status; + } + + ConnWriteF(queue->conn, "QRETR %s MESSAGE\r\n", queue->qID); + ConnFlush(queue->conn); + ConnReadAnswer(queue->conn, queue->line, CONN_BUFSIZE); + if (atoi(queue->line) != 2023) { + status = BONGO_MAILAUTH_IO_ERROR; + goto cleanup; + } + remaining = atoll(&queue->line[5]); + while (remaining > 0) { + size_t lineLength; + size_t consumed; + + ConnReadToAllocatedBuffer(queue->conn, &messageLine, + &messageLineSize); + if (messageLine == NULL) { + status = BONGO_MAILAUTH_IO_ERROR; + goto cleanup; + } + lineLength = strlen(messageLine); + status = BongoMailAuthDKIMSignerUpdate(signer, messageLine, lineLength); + if (status == BONGO_MAILAUTH_OK) { + status = BongoMailAuthDKIMSignerUpdate(signer, "\r\n", 2U); + } + if (status != BONGO_MAILAUTH_OK) { + goto cleanup; + } + consumed = lineLength + 2U; + remaining -= remaining > (long long) consumed + ? (long long) consumed : remaining; + } + ConnReadAnswer(queue->conn, queue->line, CONN_BUFSIZE); + if (atoi(queue->line) != 1000) { + status = BONGO_MAILAUTH_IO_ERROR; + goto cleanup; + } + status = BongoMailAuthDKIMSignerFinish( + signer, queue->dkimSignature, sizeof(queue->dkimSignature)); + queue->hasDKIMSignature = status == BONGO_MAILAUTH_OK; + +cleanup: + if (messageLine != NULL) { + MemFree(messageLine); + } + BongoMailAuthDKIMSignerFree(signer); + return status; +} static void SMTPClientFree(void *clientp) @@ -70,7 +234,7 @@ int RecipientCompare(const void *lft, const void *rgt) { } Connection *LookupRemoteMX(RecipStruct *Recipient) { - unsigned char Host[MAXEMAILNAMESIZE+1]; + char Host[MAXEMAILNAMESIZE+1]; XplDns_MxLookup *mx = NULL; XplDns_IpList *list = NULL; Connection *Result=NULL; @@ -79,7 +243,7 @@ Connection *LookupRemoteMX(RecipStruct *Recipient) { *(Recipient->localPart-1) = '\0'; if (Recipient->SortField[0] == '[') { // potential ip address in the host name (e.g. jdoe@[1.1.1.1]) - unsigned char *end; + char *end; end = strchr(Recipient->SortField, ']'); if (end) { *end = '\0'; @@ -181,14 +345,113 @@ finish: return Result; } +static BOOL +FindLMTPTransport(const RecipStruct *recipient, char *host, size_t hostSize, + int *port) +{ + const unsigned char *at; + unsigned int index; + + if (MailAuth.lmtp_transports == NULL || host == NULL || hostSize == 0U || + port == NULL) { + return FALSE; + } + at = (const unsigned char *) strrchr((const char *) recipient->To, '@'); + if (at == NULL) { + return FALSE; + } + for (index = 0U; index < MailAuth.lmtp_transports->len; index++) { + const char *entry = g_array_index(MailAuth.lmtp_transports, char *, index); + const char *equals = entry != NULL ? strchr(entry, '=') : NULL; + const char *colon = equals != NULL ? strrchr(equals + 1, ':') : NULL; + size_t domainLength; + size_t hostLength; + char *end = NULL; + long parsedPort; + + if (equals == NULL || colon == NULL || equals == entry || + colon == equals + 1 || colon[1] == '\0') { + continue; + } + domainLength = (size_t) (equals - entry); + if (strlen((const char *) at + 1) != domainLength || + strncasecmp((const char *) at + 1, entry, domainLength) != 0) { + continue; + } + hostLength = (size_t) (colon - (equals + 1)); + if (hostLength == 0U || hostLength >= hostSize) { + continue; + } + parsedPort = strtol(colon + 1, &end, 10); + if (end == colon + 1 || *end != '\0' || parsedPort <= 0L || + parsedPort > 65535L) { + continue; + } + memcpy(host, equals + 1, hostLength); + host[hostLength] = '\0'; + *port = (int) parsedPort; + return TRUE; + } + return FALSE; +} + +static Connection * +ConnectLMTP(RecipStruct *recipient, const char *host, int targetPort) +{ + struct addrinfo hints; + struct addrinfo *addresses = NULL; + struct addrinfo *address; + char port[16]; + Connection *connection = NULL; + int error; + + if (host == NULL || host[0] == '\0' || targetPort <= 0 || + targetPort > 65535) { + recipient->Result = DELIVER_BOGUS_NAME; + return NULL; + } + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + snprintf(port, sizeof(port), "%d", targetPort); + error = getaddrinfo(host, port, &hints, &addresses); + if (error != 0) { + recipient->Result = error == EAI_AGAIN + ? DELIVER_TIMEOUT : DELIVER_HOST_UNKNOWN; + return NULL; + } + for (address = addresses; address != NULL; address = address->ai_next) { + const struct sockaddr_in *socketAddress = + (const struct sockaddr_in *) address->ai_addr; + + connection = ConnAlloc(TRUE); + if (connection == NULL) { + recipient->Result = DELIVER_TRY_LATER; + break; + } + connection->socketAddress = *socketAddress; + if (ConnConnect(connection, NULL, 0, NULL) > 0) { + break; + } + ConnFree(connection); + connection = NULL; + recipient->Result = errno == ETIMEDOUT + ? DELIVER_TIMEOUT : DELIVER_TRY_LATER; + } + freeaddrinfo(addresses); + return connection; +} + BOOL DeliverMessage(SMTPClient *Queue, SMTPClient *Remote, RecipStruct *Recip) { int Extensions; unsigned long Size=0, MessageLineLen=0; long long MessageLength = 0; int Ret; - unsigned char TimeBuf[80]; + char TimeBuf[80]; char *MessageLine=NULL; + char rewrittenSender[1024]; + const char *envelopeSender = NULL; /* we are connected to the remote smtp server, but we haven't done any conversation yet */ /* read out the banner */ @@ -210,11 +473,12 @@ DeliverMessage(SMTPClient *Queue, SMTPClient *Remote, RecipStruct *Recip) { beginConversation: Extensions = 0; - ConnWriteF(Remote->conn, "EHLO %s\r\n", BongoGlobals.hostname); + ConnWriteF(Remote->conn, "%s %s\r\n", + Remote->isLMTP ? "LHLO" : "EHLO", BongoGlobals.hostname); ConnFlush(Remote->conn); ConnReadAnswer(Remote->conn, Remote->line, CONN_BUFSIZE); - if (Remote->line[0] != '2' ) { + if (Remote->line[0] != '2' && !Remote->isLMTP) { /* the remote server doesn't seem to support ehlo. they should upgrade to bongo! */ ConnWriteF(Remote->conn, "HELO %s\r\n", BongoGlobals.hostname); ConnFlush(Remote->conn); @@ -234,7 +498,7 @@ beginConversation: } else if (strcmp(&Remote->line[4], "STARTTLS") == 0) { Extensions |= EXT_TLS; } else if (strcmp(&Remote->line[4], "SIZE") == 0) { - unsigned char *ptr; + char *ptr; ptr = strchr(&Remote->line[4], ' '); if (ptr) { Extensions |= EXT_SIZE; @@ -263,7 +527,8 @@ beginConversation: } /* begin the delivery process */ - if ((Extensions & EXT_SIZE) && (Queue->messageLength > Size)) { + if ((Extensions & EXT_SIZE) && + ((unsigned long)Queue->messageLength > Size)) { /* the message is too big */ ConnWrite(Remote->conn, "QUIT\r\n", 6); ConnFlush(Remote->conn); @@ -271,8 +536,17 @@ beginConversation: goto finalization; } - /* mail from */ - Ret = ConnWriteF(Remote->conn, "MAIL FROM: <%s>", Queue->sender); + /* Apply SRS only to unauthenticated externally sourced messages which + * Bongo is forwarding. Local submissions and bounces are not rewritten. */ + if (GetEnvelopeSender(Queue, rewrittenSender, sizeof(rewrittenSender), + &envelopeSender) != BONGO_MAILAUTH_OK) { + Log(LOG_ERROR, + "SRS forwarding failed for queue %s; delivery deferred", + Queue->qID); + Recip->Result = DELIVER_TRY_LATER; + goto finalization; + } + Ret = ConnWriteF(Remote->conn, "MAIL FROM: <%s>", envelopeSender); if (Extensions & EXT_SIZE) { Ret += ConnWriteF(Remote->conn, " SIZE=%u", Queue->messageLength); } @@ -313,15 +587,15 @@ beginConversation: ConnWriteF(Remote->conn, "RCPT TO: <%s>", Recip->To); if (Extensions & EXT_DSN) { - if (Recip->ORecip) { + if (Recip->ORecip[0] != '\0') { /* apparently some emails can come through with rfc822; on them?? */ if (strncasecmp(Recip->ORecip, "rfc822;", 7) == 0) { /* we are already set up and can just send the value through. possibly a relay? */ ConnWriteF(Remote->conn, " ORCPT=%s", Recip->ORecip); } else { /* we need to translate the ORecip to xtext as defined in rfc 1891 */ - unsigned char *ptr = Recip->ORecip; - unsigned char Hex[] = "012345678ABCDEF"; + const char *ptr = Recip->ORecip; + static const char Hex[] = "0123456789ABCDEF"; ConnWriteStr(Remote->conn, " ORCPT=rfc822;"); while (*ptr) { @@ -413,6 +687,10 @@ beginConversation: Queue->conn->socketAddress.sin_addr.s_impno, BongoGlobals.hostname, TimeBuf); + if (Queue->hasDKIMSignature) { + ConnWriteF(Remote->conn, "DKIM-Signature: %s\r\n", + Queue->dkimSignature); + } /* stream the message over */ ConnWriteF(Queue->conn, "QRETR %s MESSAGE\r\n", Queue->qID); @@ -517,7 +795,7 @@ ProcessEntry(void *clientp, Connection *conn) * i need to write out all lines as i go */ envelopeLine = Queue->envelope; while (*envelopeLine) { - unsigned char *eol = envelopeLine + strlen(envelopeLine); + char *eol = envelopeLine + strlen(envelopeLine); switch(*envelopeLine) { case QUEUE_FROM : @@ -542,7 +820,7 @@ ProcessEntry(void *clientp, Connection *conn) envelopeLine++; memset(&CurrentRecip, 0, sizeof(RecipStruct)); /* add the recipient into array of recipients */ - unsigned char *orecip, *flags, *ptr; + char *orecip, *flags, *ptr; CurrentRecip.Flags = 0; CurrentRecip.Result = 0; @@ -598,13 +876,23 @@ ProcessEntry(void *clientp, Connection *conn) } /* now that we've got all recipients and locations, let's sort them to use connections more efficiently */ + { + BongoMailAuthStatus dkimStatus = GenerateDKIMSignature(Queue); + if (dkimStatus != BONGO_MAILAUTH_OK) { + Log(LOG_ERROR, + "DKIM signing preparation failed for queue %s (status %d)", + Queue->qID, (int) dkimStatus); + g_array_free(Recipients, TRUE); + return -1; + } + } g_array_sort(Recipients, (ArrayCompareFunc)RecipientCompare); /* now i can skip over any duplicates an do lookups once per remote domain */ for(startLocation=0;startLocationlen;startLocation++) { SMTPClient Remote; RecipStruct NextRecip; - unsigned char *lft; + char *lft; CurrentRecip = g_array_index(Recipients, RecipStruct, startLocation); lft = CurrentRecip.SortField; @@ -622,7 +910,13 @@ ProcessEntry(void *clientp, Connection *conn) /* by the time i get here, CurrentRecip should be the last recip * of any string of duplicates */ - Remote.conn = LookupRemoteMX(&CurrentRecip); + Remote.isLMTP = FindLMTPTransport( + &CurrentRecip, Remote.transportHost, + sizeof(Remote.transportHost), &Remote.transportPort); + Remote.conn = Remote.isLMTP + ? ConnectLMTP(&CurrentRecip, Remote.transportHost, + Remote.transportPort) + : LookupRemoteMX(&CurrentRecip); if (!Remote.conn) { /* there was an error looking up or connecting to the remote server * if i get an error in this stage of the process i need to just skip @@ -682,6 +976,8 @@ SMTPAgentServer(void *ignored) int maxThreads; int minSleep; + UNUSED_PARAMETER(ignored); + XplRenameThread(XplGetThreadID(), AGENT_DN " Server"); BongoQueueAgentGetThreadPoolParameters(&SMTPAgent.agent, &minThreads, &maxThreads, &minSleep); @@ -716,6 +1012,10 @@ ReadConfiguration(void) return FALSE; } + if (! ReadBongoConfiguration(MailAuthConfig, "smtp")) { + return FALSE; + } + return TRUE; } @@ -734,6 +1034,9 @@ XplServiceMain(int argc, char *argv[]) int startupOpts; ConnSSLConfiguration sslconfig; + UNUSED_PARAMETER(argc); + UNUSED_PARAMETER(argv); + if (XplSetRealUser(MsgGetUnprivilegedUser()) < 0) { XplConsolePrintf(AGENT_NAME ": Could not drop to unprivileged user '%s'\r\n" AGENT_NAME ": exiting.\n", MsgGetUnprivilegedUser()); return -1; diff --git a/src/agents/smtp/smtpc.h b/src/agents/smtp/smtpc.h index 5093a83..9c047c9 100644 --- a/src/agents/smtp/smtpc.h +++ b/src/agents/smtp/smtpc.h @@ -37,15 +37,20 @@ typedef struct { Connection *conn; - unsigned int envelopeLength; - unsigned int messageLength; - unsigned int envelopeLines; + int envelopeLength; + int messageLength; + int envelopeLines; char *envelope; char *sender; /* pointer into the envelope of the from address */ char *authSender; /* pointer into the envelope of the envelope from */ int flags; char line[CONN_BUFSIZE + 1]; char qID[16]; /* holds the queueid pulled during handshake */ + BOOL hasDKIMSignature; + char dkimSignature[8192]; + BOOL isLMTP; + char transportHost[256]; + int transportPort; } SMTPClient; typedef struct _SMTPAgentGlobals { @@ -62,10 +67,10 @@ typedef struct _SMTPAgentGlobals { } SMTPAgentGlobals; typedef struct { - unsigned char To[MAXEMAILNAMESIZE+1]; - unsigned char ORecip[MAXEMAILNAMESIZE+1]; - unsigned char SortField[MAXEMAILNAMESIZE+2]; /* will be formatted domain.com@emailaddr -- used for comparisons and sorting */ - unsigned char *localPart; /* pointer into the SortField for the beginning of the localPart */ + char To[MAXEMAILNAMESIZE+1]; + char ORecip[MAXEMAILNAMESIZE+1]; + char SortField[MAXEMAILNAMESIZE+2]; /* will be formatted domain.com@emailaddr -- used for comparisons and sorting */ + char *localPart; /* pointer into the SortField for the beginning of the localPart */ size_t ToLen; /* just so we don't have to calculate the length a bunch of times */ unsigned long Flags; int Result; diff --git a/src/agents/smtp/smtpd.c b/src/agents/smtp/smtpd.c index 8f41b57..d2370a8 100644 --- a/src/agents/smtp/smtpd.c +++ b/src/agents/smtp/smtpd.c @@ -43,12 +43,31 @@ #include #include +#include "mailauth.h" #include "smtpd.h" struct { int port; int port_ssl; + BOOL submission_enabled; + int submission_port; + int submission_port_ssl; + BOOL submission_require_auth; + BOOL submission_allow_sender_override; + BOOL internal_relay_enabled; + int internal_relay_port; + char *internal_relay_bind_address; + GArray *internal_relay_networks; + BOOL internal_relay_allow_legacy_tls; + int internal_relay_messages_per_minute; + int internal_relay_recipients_per_minute; + int internal_relay_bytes_per_minute; + int internal_relay_connections_per_ip; + char *internal_relay_domain; + GArray *internal_relay_device_mappings; + GArray *internal_relay_dns_suffixes; BOOL allow_client_ssl; + BOOL allow_legacy_tls; BOOL allow_expn; BOOL allow_auth; BOOL require_auth; @@ -67,12 +86,52 @@ struct { int max_null_sender; int max_mx_servers; int socket_timeout; + BOOL spf_verify; + BOOL dkim_verify; + BOOL dmarc_verify; + BOOL dmarc_enforce; + BOOL srs_reverse; + char *srs_domain; + char *srs_secret_file; + GArray *lmtp_transports; } SMTP; +static BongoConfigItem LMTPTransportList = { + BONGO_JSON_STRING, NULL, &SMTP.lmtp_transports +}; + +static BongoConfigItem InternalRelayNetworkList = { + BONGO_JSON_STRING, NULL, &SMTP.internal_relay_networks +}; +static BongoConfigItem InternalRelayDeviceMappingList = { + BONGO_JSON_STRING, NULL, &SMTP.internal_relay_device_mappings +}; +static BongoConfigItem InternalRelayDNSSuffixList = { + BONGO_JSON_STRING, NULL, &SMTP.internal_relay_dns_suffixes +}; + static BongoConfigItem SMTPConfig[] = { { BONGO_JSON_INT, "o:port/i", &SMTP.port }, { BONGO_JSON_INT, "o:port_ssl/i", &SMTP.port_ssl }, + { BONGO_JSON_BOOL, "o:submission_enabled/b", &SMTP.submission_enabled }, + { BONGO_JSON_INT, "o:submission_port/i", &SMTP.submission_port }, + { BONGO_JSON_INT, "o:submission_port_ssl/i", &SMTP.submission_port_ssl }, + { BONGO_JSON_BOOL, "o:submission_require_auth/b", &SMTP.submission_require_auth }, + { BONGO_JSON_BOOL, "o:submission_allow_sender_override/b", &SMTP.submission_allow_sender_override }, + { BONGO_JSON_BOOL, "o:internal_relay_enabled/b", &SMTP.internal_relay_enabled }, + { BONGO_JSON_INT, "o:internal_relay_port/i", &SMTP.internal_relay_port }, + { BONGO_JSON_STRING, "o:internal_relay_bind_address/s", &SMTP.internal_relay_bind_address }, + { BONGO_JSON_ARRAY, "o:internal_relay_networks/a", &InternalRelayNetworkList }, + { BONGO_JSON_BOOL, "o:internal_relay_allow_legacy_tls/b", &SMTP.internal_relay_allow_legacy_tls }, + { BONGO_JSON_INT, "o:internal_relay_messages_per_minute/i", &SMTP.internal_relay_messages_per_minute }, + { BONGO_JSON_INT, "o:internal_relay_recipients_per_minute/i", &SMTP.internal_relay_recipients_per_minute }, + { BONGO_JSON_INT, "o:internal_relay_bytes_per_minute/i", &SMTP.internal_relay_bytes_per_minute }, + { BONGO_JSON_INT, "o:internal_relay_connections_per_ip/i", &SMTP.internal_relay_connections_per_ip }, + { BONGO_JSON_STRING, "o:internal_relay_domain/s", &SMTP.internal_relay_domain }, + { BONGO_JSON_ARRAY, "o:internal_relay_device_mappings/a", &InternalRelayDeviceMappingList }, + { BONGO_JSON_ARRAY, "o:internal_relay_dns_suffixes/a", &InternalRelayDNSSuffixList }, { BONGO_JSON_BOOL, "o:allow_client_ssl/b", &SMTP.allow_client_ssl }, + { BONGO_JSON_BOOL, "o:allow_legacy_tls/b", &SMTP.allow_legacy_tls }, { BONGO_JSON_BOOL, "o:allow_expn/b", &SMTP.allow_expn }, { BONGO_JSON_BOOL, "o:allow_vrfy/b", &SMTP.allow_vrfy }, { BONGO_JSON_BOOL, "o:allow_auth/b", &SMTP.allow_auth }, @@ -91,6 +150,14 @@ static BongoConfigItem SMTPConfig[] = { { BONGO_JSON_INT, "o:max_mx_servers/i", &SMTP.max_mx_servers }, { BONGO_JSON_BOOL, "o:relay_local_mail/b", &SMTP.relay_local }, { BONGO_JSON_BOOL, "o:require_auth/b", &SMTP.require_auth }, + { BONGO_JSON_BOOL, "o:spf_verify/b", &SMTP.spf_verify }, + { BONGO_JSON_BOOL, "o:dkim_verify/b", &SMTP.dkim_verify }, + { BONGO_JSON_BOOL, "o:dmarc_verify/b", &SMTP.dmarc_verify }, + { BONGO_JSON_BOOL, "o:dmarc_enforce/b", &SMTP.dmarc_enforce }, + { BONGO_JSON_BOOL, "o:srs_reverse/b", &SMTP.srs_reverse }, + { BONGO_JSON_STRING, "o:srs_domain/s", &SMTP.srs_domain }, + { BONGO_JSON_STRING, "o:srs_secret_file/s", &SMTP.srs_secret_file }, + { BONGO_JSON_ARRAY, "o:lmtp_transports/a", &LMTPTransportList }, { BONGO_JSON_NULL, NULL, NULL } }; @@ -104,6 +171,8 @@ XplAtomic SMTPIdleConnThreads; XplAtomic SMTPQueueThreads; Connection *SMTPServerConnection; Connection *SMTPServerConnectionSSL; +Connection *SMTPInternalServerConnection; +Connection *SMTPSubmissionServerConnection; Connection *SMTPQServerConnection; Connection *SMTPQServerConnectionSSL; int TGid; @@ -122,6 +191,17 @@ unsigned long BounceCount; char NMAPServer[20] = "127.0.0.1"; unsigned long LocalAddress = 0; bongo_ssl_context *SSLContext = NULL; +static bongo_ssl_context *InternalSSLContext = NULL; +static GHashTable *InternalRelayRates; +static XplMutex InternalRelayRateLock; + +typedef struct { + time_t window; + unsigned int messages; + unsigned int recipients; + unsigned long bytes; + unsigned int connections; +} InternalRelayRate; static char NMAPHash[NMAP_HASH_SIZE]; #ifdef USE_HOPCOUNT_DETECTION @@ -174,8 +254,311 @@ typedef struct char *AuthFrom; /* Sender Authenticated As */ char User[64]; /* Fixme - Make this bigger */ BOOL IsEHLO; /* used for RFC 3848 */ + BOOL HasSPFResult; + BongoSPFResponse SPFResult; + BOOL InternalRelay; + BOOL Submission; + InternalRelayRate *InternalRate; + char InternalDevice[64]; } ConnectionStruct; +static BOOL +InternalRelayConsume(ConnectionStruct *client, unsigned int messages, + unsigned int recipients, unsigned long bytes) +{ + InternalRelayRate *rate = client->InternalRate; + time_t now = time(NULL); + BOOL allowed = TRUE; + + if (!client->InternalRelay || rate == NULL) return TRUE; + XplMutexLock(InternalRelayRateLock); + if (now < rate->window || now - rate->window >= 60) { + rate->window = now; + rate->messages = rate->recipients = 0; + rate->bytes = 0; + } + if ((messages && rate->messages + messages > (unsigned int) SMTP.internal_relay_messages_per_minute) || + (recipients && rate->recipients + recipients > (unsigned int) SMTP.internal_relay_recipients_per_minute) || + (bytes && rate->bytes + bytes > (unsigned long) SMTP.internal_relay_bytes_per_minute)) { + allowed = FALSE; + } else { + rate->messages += messages; + rate->recipients += recipients; + rate->bytes += bytes; + } + XplMutexUnlock(InternalRelayRateLock); + return allowed; +} + +static BOOL +NormalizeInternalSender(const char *sender, const char *device, char *normalized, + size_t normalizedSize) +{ + const char *at; + const char *domain; + size_t localLength; + size_t domainLength; + + if (sender == NULL || normalized == NULL || normalizedSize == 0 || + SMTP.internal_relay_domain == NULL || *SMTP.internal_relay_domain == '\0') return FALSE; + at = strrchr(sender, '@'); + if (at == NULL || at == sender || at[1] == '\0') return FALSE; + domain = at + 1; + localLength = (size_t) (at - sender); + domainLength = strlen(domain); + if (strcasecmp(domain, "localhost") == 0 || + strcasecmp(domain, "localdomain") == 0 || + strcasecmp(domain, "localhost.localdomain") == 0) { + if (device != NULL && *device != '\0' && + (localLength == 4 && strncasecmp(sender, "root", 4) == 0)) { + return snprintf(normalized, normalizedSize, "%.*s+%s@%s", (int) localLength, + sender, device, SMTP.internal_relay_domain) < (int) normalizedSize; + } + return snprintf(normalized, normalizedSize, "%.*s@%s", (int) localLength, + sender, SMTP.internal_relay_domain) < (int) normalizedSize; + } + if (domainLength > strlen(SMTP.internal_relay_domain) + 1 && + domain[domainLength - strlen(SMTP.internal_relay_domain) - 1] == '.' && + strcasecmp(domain + domainLength - strlen(SMTP.internal_relay_domain), + SMTP.internal_relay_domain) == 0) { + size_t subdomainLength = domainLength - strlen(SMTP.internal_relay_domain) - 1; + return snprintf(normalized, normalizedSize, "%.*s+%.*s@%s", (int) localLength, + sender, (int) subdomainLength, domain, + SMTP.internal_relay_domain) < (int) normalizedSize; + } + return FALSE; +} + +static BOOL +AddressInNetwork(struct in_addr address, const char *network) +{ + char text[INET_ADDRSTRLEN + 4]; + char *slash; + char *end; + struct in_addr base; + unsigned long prefix = 32; + uint32_t mask; + + if (network == NULL || strlen(network) >= sizeof(text)) { + return FALSE; + } + strcpy(text, network); + slash = strchr(text, '/'); + if (slash != NULL) { + *slash++ = '\0'; + errno = 0; + prefix = strtoul(slash, &end, 10); + if (errno != 0 || *slash == '\0' || *end != '\0' || prefix > 32) { + return FALSE; + } + } + if (inet_pton(AF_INET, text, &base) != 1) { + return FALSE; + } + mask = prefix == 0 ? 0 : UINT32_MAX << (32 - prefix); + return (ntohl(address.s_addr) & mask) == (ntohl(base.s_addr) & mask); +} + +static BOOL +InternalRelayAddressAllowed(struct in_addr address) +{ + unsigned int index; + + for (index = 0; SMTP.internal_relay_networks != NULL && + index < SMTP.internal_relay_networks->len; index++) { + const char *network = g_array_index(SMTP.internal_relay_networks, char *, index); + if (AddressInNetwork(address, network)) { + return TRUE; + } + } + return FALSE; +} + +static BOOL +ValidDeviceName(const char *name) +{ + const unsigned char *p = (const unsigned char *) name; + if (p == NULL || *p == '\0') return FALSE; + while (*p != '\0') { + if (!isalnum(*p) && *p != '-') return FALSE; + p++; + } + return TRUE; +} + +static BOOL +HostHasAllowedSuffix(const char *host) +{ + unsigned int index; + size_t hostLength = strlen(host); + for (index = 0; SMTP.internal_relay_dns_suffixes != NULL && + index < SMTP.internal_relay_dns_suffixes->len; index++) { + const char *suffix = g_array_index(SMTP.internal_relay_dns_suffixes, char *, index); + size_t suffixLength = suffix != NULL ? strlen(suffix) : 0; + if (suffixLength && hostLength > suffixLength && + host[hostLength - suffixLength - 1] == '.' && + strcasecmp(host + hostLength - suffixLength, suffix) == 0) return TRUE; + } + return FALSE; +} + +static BOOL +IdentifyInternalDevice(struct in_addr address, char *device, size_t deviceSize) +{ + unsigned int index; + char ip[INET_ADDRSTRLEN]; + char host[NI_MAXHOST]; + struct sockaddr_in peer; + struct addrinfo hints; + struct addrinfo *answers = NULL; + struct addrinfo *answer; + BOOL forwardMatch = FALSE; + + if (inet_ntop(AF_INET, &address, ip, sizeof(ip)) == NULL) return FALSE; + for (index = 0; SMTP.internal_relay_device_mappings != NULL && + index < SMTP.internal_relay_device_mappings->len; index++) { + const char *mapping = g_array_index(SMTP.internal_relay_device_mappings, char *, index); + const char *equals = mapping != NULL ? strchr(mapping, '=') : NULL; + if (equals != NULL && (size_t) (equals - mapping) == strlen(ip) && + strncmp(mapping, ip, strlen(ip)) == 0 && ValidDeviceName(equals + 1)) { + snprintf(device, deviceSize, "%s", equals + 1); + return TRUE; + } + } + memset(&peer, 0, sizeof(peer)); + peer.sin_family = AF_INET; + peer.sin_addr = address; + if (getnameinfo((struct sockaddr *) &peer, sizeof(peer), host, sizeof(host), + NULL, 0, NI_NAMEREQD) != 0 || !HostHasAllowedSuffix(host)) return FALSE; + memset(&hints, 0, sizeof(hints)); + hints.ai_family = AF_INET; + hints.ai_socktype = SOCK_STREAM; + if (getaddrinfo(host, NULL, &hints, &answers) != 0) return FALSE; + for (answer = answers; answer != NULL; answer = answer->ai_next) { + struct sockaddr_in *resolved = (struct sockaddr_in *) answer->ai_addr; + if (resolved->sin_addr.s_addr == address.s_addr) { + forwardMatch = TRUE; + break; + } + } + freeaddrinfo(answers); + if (forwardMatch) { + char *dot = strchr(host, '.'); + if (dot != NULL) *dot = '\0'; + if (ValidDeviceName(host)) { + snprintf(device, deviceSize, "%s", host); + return TRUE; + } + } + return FALSE; +} + +static const char * +SPFResultName(BongoSPFResult result) +{ + static const char *const names[] = { + "none", "neutral", "pass", "fail", "softfail", "none", + "temperror", "permerror" + }; + + return result >= BONGO_SPF_INVALID && result <= BONGO_SPF_PERMERROR + ? names[result] : "none"; +} + +static BOOL +IsTransportRecipient(const char *address) +{ + const char *at; + unsigned int index; + + if (address == NULL || SMTP.lmtp_transports == NULL || + (at = strrchr(address, '@')) == NULL || at[1] == '\0') { + return FALSE; + } + for (index = 0U; index < SMTP.lmtp_transports->len; index++) { + const char *entry = g_array_index(SMTP.lmtp_transports, char *, index); + const char *equals = entry != NULL ? strchr(entry, '=') : NULL; + size_t domainLength; + + if (equals == NULL || equals == entry) { + continue; + } + domainLength = (size_t) (equals - entry); + if (strlen(at + 1) == domainLength && + strncasecmp(at + 1, entry, domainLength) == 0) { + return TRUE; + } + } + return FALSE; +} + +static BOOL +AuthenticateUser(ConnectionStruct *client, const char *user, const char *password) +{ + if (user == NULL || password == NULL || *user == '\0' || + MsgAuthFindUser(user) != 0 || MsgAuthVerifyPassword(user, password) != 0) { + Log(LOG_NOTICE, "Authentication failed for user %s at host %s", + user != NULL ? user : "", LOGIP(client->client.conn->socketAddress)); + return FALSE; + } + + Log(LOG_NOTICE, "Successful login for user %s at host %s", user, + LOGIP(client->client.conn->socketAddress)); + MemFree(client->AuthFrom); + client->AuthFrom = MemStrdup(user); + return client->AuthFrom != NULL; +} + +static BOOL +SubmissionSenderAllowed(const char *user, const char *sender) +{ + const char *at; + size_t localLength; + if (SMTP.submission_allow_sender_override || user == NULL || sender == NULL || + *sender == '\0') return TRUE; + if (strcasecmp(user, sender) == 0) return TRUE; + if (strchr(user, '@') != NULL) return FALSE; + at = strrchr(sender, '@'); + localLength = at != NULL ? (size_t) (at - sender) : strlen(sender); + return strlen(user) == localLength && strncasecmp(user, sender, localLength) == 0; +} + +static BOOL +ParsePlainCredentials(char *encoded, const char **user, const char **password) +{ + size_t encodedLength; + size_t decodedLimit; + char *decoded; + char *authcid; + char *separator; + + if (encoded == NULL || user == NULL || password == NULL) { + return FALSE; + } + encodedLength = strlen(encoded); + if (encodedLength == 0 || encodedLength > BUFSIZE) { + return FALSE; + } + decodedLimit = ((encodedLength + 3U) / 4U) * 3U; + decoded = DecodeBase64(encoded); + if (decoded == NULL) { + return FALSE; + } + + separator = memchr(decoded, '\0', decodedLimit); + if (separator == NULL) { + return FALSE; + } + authcid = separator + 1; + separator = memchr(authcid, '\0', decodedLimit - (size_t)(authcid - decoded)); + if (separator == NULL || authcid == separator) { + return FALSE; + } + *user = authcid; + *password = separator + 1; + return (size_t)(*password - decoded) < decodedLimit; +} + typedef struct { char To[MAXEMAILNAMESIZE+1]; @@ -200,7 +583,7 @@ void *SMTPConnectionPool = NULL; /* Prototypes */ long ReadConnection(Connection *conn, char **buffer, unsigned long *buflen); void ProcessRemoteEntry (ConnectionStruct * Client, unsigned long Size, int Lines); -int RewriteAddress (Connection * conn, char *Source, char *Target, unsigned int TargetSize); +int RewriteAddress (Connection * conn, const char *Source, char *Target, unsigned int TargetSize); BOOL FlushClient (ConnectionStruct * Client); BOOL EndClientConnection (ConnectionStruct * Client); static int PullLine (char *Line, unsigned long LineSize, char **NextLine); @@ -333,6 +716,12 @@ EndClientConnection (ConnectionStruct * Client) MemFree (Client->AuthFrom); Client->AuthFrom = NULL; } + if (Client->InternalRate != NULL) { + XplMutexLock(InternalRelayRateLock); + if (Client->InternalRate->connections > 0) Client->InternalRate->connections--; + XplMutexUnlock(InternalRelayRateLock); + Client->InternalRate = NULL; + } /* Bump our thread count */ if (Client->Flags < STATE_WAITING) { @@ -352,7 +741,7 @@ HandleConnection (void *param) { ConnectionStruct *Client = (ConnectionStruct *) param; int count; - int ReplyInt; + int ReplyInt = 0; BOOL Ready; BOOL Working = TRUE; BOOL IsTrusted = TRUE; @@ -369,9 +758,18 @@ HandleConnection (void *param) time (&connectionTime); + if (Client->InternalRelay && + IdentifyInternalDevice(Client->client.conn->socketAddress.sin_addr, + Client->InternalDevice, + sizeof(Client->InternalDevice))) { + Log(LOG_INFO, "Identified internal SMTP device %s at %s", + Client->InternalDevice, LOGIP(Client->client.conn->socketAddress)); + } + if (Client->client.conn->ssl.enable) { Log(LOG_INFO, "SSL connection from %s", LOGIP(Client->client.conn->socketAddress)); - if (!ConnNegotiate(Client->client.conn, SSLContext)) { + if (!ConnNegotiate(Client->client.conn, + Client->InternalRelay ? InternalSSLContext : SSLContext)) { return (EndClientConnection (Client)); } } else { @@ -379,7 +777,7 @@ HandleConnection (void *param) } XplRWReadLockAcquire (&ConfigLock); - if (! SMTP.allow_auth) { + if (!SMTP.allow_auth || Client->InternalRelay) { AllowAuth = FALSE; } @@ -397,8 +795,8 @@ HandleConnection (void *param) Log(LOG_ERROR, "Unable to authenticate to Queue agent at %s", LOGIP(Client->client.conn->socketAddress)); } else { - Log(LOG_ERROR, "Unable to connect to Queue agent at %s (reply was %d)", - LOGIP(Client->client.conn->socketAddress), ReplyInt); + Log(LOG_ERROR, "Unable to connect to Queue agent at %s", + LOGIP(Client->client.conn->socketAddress)); } count = snprintf(Reply, sizeof(Reply), "421 %s %s\r\n", BongoGlobals.hostname, MSG421SHUTDOWN); @@ -447,8 +845,8 @@ HandleConnection (void *param) if (Client->State == STATE_FRESH) { snprintf (Answer, sizeof (Answer), "250 %s %s\r\n", BongoGlobals.hostname, MSG250HELLO); ConnWrite (Client->client.conn, Answer, strlen (Answer)); - strncpy (Client->RemoteHost, Client->Command + 5, - sizeof (Client->RemoteHost)); + snprintf(Client->RemoteHost, sizeof(Client->RemoteHost), + "%s", Client->Command + 5); Client->State = STATE_HELO; NullSender = FALSE; @@ -472,7 +870,8 @@ HandleConnection (void *param) case 'S':{ /* SAML, SOML, SEND */ switch (toupper (Client->Command[1])) { case 'T':{ /* STARTTLS */ - if (!SMTP.allow_client_ssl) { + if ((!Client->InternalRelay && !Client->Submission && !SMTP.allow_client_ssl) || + (Client->InternalRelay && InternalSSLContext == NULL)) { ConnWrite (Client->client.conn, MSG500UNKNOWN, MSG500UNKNOWN_LEN); break; } @@ -483,7 +882,8 @@ HandleConnection (void *param) /* try to negotiate the connection */ Client->client.conn->ssl.enable = TRUE; - if (ConnNegotiate(Client->client.conn, SSLContext)) { + if (ConnNegotiate(Client->client.conn, + Client->InternalRelay ? InternalSSLContext : SSLContext)) { /* negotiation worked */ Client->State = STATE_FRESH; } @@ -507,74 +907,83 @@ HandleConnection (void *param) } case 'A':{ /* AUTH */ - char *PW; + const char *user = NULL; + const char *password = NULL; + BOOL authenticated = FALSE; - if (AllowAuth == FALSE) { + if (AllowAuth == FALSE || !Client->client.conn->ssl.enable) { ConnWrite (Client->client.conn, MSG500UNKNOWN, MSG500UNKNOWN_LEN); break; } - /* rfc 2554 */ + /* RFC 4954 */ if (IsAuthed) { ConnWrite (Client->client.conn, MSG503BADORDER, MSG503BADORDER_LEN); break; - } - - if (XplStrNCaseCmp (Client->Command + 5, "LOGIN", 5) != 0) { - ConnWrite (Client->client.conn, MSG504BADAUTH, MSG504BADAUTH_LEN); - break; } - if (!isspace (Client->Command[10])) { + + if (XplStrNCaseCmp(Client->Command + 5, "PLAIN", 5) == 0) { + char *encoded = Client->Command + 10; + + while (isspace((unsigned char)*encoded)) { + encoded++; + } + if (*encoded == '\0') { + ConnWrite(Client->client.conn, "334 \r\n", 6); + ConnFlush(Client->client.conn); + if (ConnReadToAllocatedBuffer(Client->client.conn, + &Client->client.buffer, &Client->client.buflen) <= 0) { + return EndClientConnection(Client); + } + encoded = Client->client.buffer; + } + if (strcmp(encoded, "*") == 0) { + ConnWrite(Client->client.conn, + "501 5.7.0 Authentication canceled\r\n", 35); + break; + } + if (ParsePlainCredentials(encoded, &user, &password)) { + authenticated = AuthenticateUser(Client, user, password); + } + } else if (XplStrNCaseCmp(Client->Command + 5, "LOGIN", 5) == 0) { + if (!isspace((unsigned char)Client->Command[10])) { ConnWrite (Client->client.conn, "334 VXNlcm5hbWU6\r\n", 18); ConnFlush (Client->client.conn); - ConnReadToAllocatedBuffer(Client->client.conn, &(Client->client.buffer), &(Client->client.buflen)); - strcpy (Reply, Client->client.buffer); - } - else { - strcpy (Reply, Client->client.buffer + 11); - } - - /* it might be possible to abort the auth here (rfc 2554). - * this is a code duplication from below */ - if (Client->client.buffer[0] == '*' && Client->client.buffer[1] == '\0') { - ConnWrite (Client->client.conn, "501 Authentication aborted by client\r\n", 38); - break; - } - - ConnWrite (Client->client.conn, "334 UGFzc3dvcmQ6\r\n", 18); - ConnFlush (Client->client.conn); - - ConnReadToAllocatedBuffer(Client->client.conn, &(Client->client.buffer), &(Client->client.buflen)); - - if (Client->client.buffer[0] == '*' && Client->client.buffer[1] == '\0') { - ConnWrite (Client->client.conn, "501 Authentication aborted by client\r\n", 38); - break; - } - - PW = DecodeBase64 (Client->client.buffer); - DecodeBase64 (Reply); - - if (MsgAuthFindUser(Reply) == 0) { - if (MsgAuthVerifyPassword(Reply, PW) != 0) { - ConnWrite (Client->client.conn, "501 Authentication failed!\r\n", 28); - Log(LOG_NOTICE, "Wrong password from user %s at host %s", - Reply, - LOGIP(Client->client.conn->socketAddress)); + if (ConnReadToAllocatedBuffer(Client->client.conn, + &Client->client.buffer, &Client->client.buflen) <= 0) { + return EndClientConnection(Client); + } + snprintf(Reply, sizeof(Reply), "%s", Client->client.buffer); } else { - Log(LOG_NOTICE, "Successful login for user %s at host %s", - Reply, - LOGIP(Client->client.conn->socketAddress)); - if (Client->AuthFrom != NULL) MemFree (Client->AuthFrom); - Client->AuthFrom = MemStrdup (Reply); - Client->State = STATE_AUTH; - ConnWrite (Client->client.conn, "235 Authentication successful!\r\n", 32); - IsAuthed = TRUE; - IsTrusted = TRUE; + snprintf(Reply, sizeof(Reply), "%s", Client->Command + 11); + } + if (strcmp(Reply, "*") != 0) { + user = DecodeBase64(Reply); + ConnWrite(Client->client.conn, "334 UGFzc3dvcmQ6\r\n", 18); + ConnFlush(Client->client.conn); + if (ConnReadToAllocatedBuffer(Client->client.conn, + &Client->client.buffer, &Client->client.buflen) <= 0) { + return EndClientConnection(Client); + } + if (strcmp(Client->client.buffer, "*") != 0) { + password = DecodeBase64(Client->client.buffer); + authenticated = AuthenticateUser(Client, user, password); + } } } else { - Log(LOG_NOTICE, "Unknown user %s at host %s", Reply, - LOGIP(Client->client.conn->socketAddress)); - ConnWrite (Client->client.conn, "501 Authentication failed!\r\n", 28); + ConnWrite(Client->client.conn, MSG504BADAUTH, MSG504BADAUTH_LEN); + break; + } + + if (authenticated) { + Client->State = STATE_AUTH; + IsAuthed = TRUE; + IsTrusted = TRUE; + ConnWrite(Client->client.conn, + "235 2.7.0 Authentication successful\r\n", 37); + } else { + ConnWrite(Client->client.conn, + "535 5.7.8 Authentication credentials invalid\r\n", 46); } break; } @@ -603,7 +1012,9 @@ HandleConnection (void *param) char temp, *name; BOOL GotFlags = FALSE; char To[MAXEMAILNAMESIZE + 1] = ""; + char SRSAddress[MAXEMAILNAMESIZE + 1] = ""; char *Orcpt = NULL; + const char *RoutingName; if (Client->State < STATE_FROM) { ConnWrite (Client->client.conn, MSG501NOSENDER, @@ -754,10 +1165,60 @@ HandleConnection (void *param) break; } - if ((ReplyInt = RewriteAddress (Client->nmap.conn, name, To, sizeof (To))) == MAIL_BOGUS) { + RoutingName = name; + if (ptr != NULL && strncasecmp(name, "SRS", 3) == 0) { + BOOL reverseSRS; + char srsDomain[MAXEMAILNAMESIZE + 1]; + char secretFile[XPL_MAX_PATH]; + + XplRWReadLockAcquire(&ConfigLock); + reverseSRS = SMTP.srs_reverse; + snprintf(srsDomain, sizeof(srsDomain), "%s", + SMTP.srs_domain != NULL ? SMTP.srs_domain : ""); + snprintf(secretFile, sizeof(secretFile), "%s", + SMTP.srs_secret_file != NULL + ? SMTP.srs_secret_file : ""); + XplRWReadLockRelease(&ConfigLock); + + if (reverseSRS && srsDomain[0] != '\0' && + strcasecmp(ptr + 1, srsDomain) == 0) { + BongoMailAuthStatus srsStatus = + BongoMailAuthSRSReverse(secretFile, name, + SRSAddress, + sizeof(SRSAddress)); + if (srsStatus == BONGO_MAILAUTH_PROTOCOL_ERROR) { + Log(LOG_NOTICE, + "Rejected invalid SRS recipient %s from host %s", + name, + LOGIP(Client->client.conn->socketAddress)); + ConnWrite(Client->client.conn, MSG550SRSINVALID, + MSG550SRSINVALID_LEN); + goto QuitRcpt; + } + if (srsStatus != BONGO_MAILAUTH_OK) { + Log(LOG_ERROR, + "Unable to validate SRS recipient %s (status %d)", + name, (int) srsStatus); + ConnWrite(Client->client.conn, + MSG451INTERNALERR, + MSG451INTERNALERR_LEN); + goto QuitRcpt; + } + RoutingName = SRSAddress; + } + } + + if ((ReplyInt = RewriteAddress (Client->nmap.conn, + RoutingName, To, + sizeof (To))) == MAIL_BOGUS) { ConnWrite (Client->client.conn, MSG501RECIPNO, MSG501RECIPNO_LEN); } else { + if (ReplyInt == MAIL_REMOTE && + IsTransportRecipient(RoutingName)) { + ReplyInt = MAIL_RELAY; + snprintf(To, sizeof(To), "%s", RoutingName); + } switch (ReplyInt) { case MAIL_REMOTE:{ if (!IsAuthed) { @@ -769,7 +1230,7 @@ HandleConnection (void *param) goto QuitRcpt; } XplRWReadLockAcquire (&ConfigLock); - if (Client->RecipCount >= SMTP.max_recips) { + if (Client->RecipCount >= (unsigned int)SMTP.max_recips) { XplRWReadLockRelease (&ConfigLock); Log(LOG_INFO, "Recipient limit from %s at host %s to user %s", Client->AuthFrom? @@ -812,7 +1273,8 @@ HandleConnection (void *param) case MAIL_LOCAL:{ XplRWReadLockAcquire (&ConfigLock); - if ((NullSender == FALSE) || (Client->RecipCount < SMTP.max_null_sender)) { + if ((NullSender == FALSE) || + (Client->RecipCount < (unsigned int)SMTP.max_null_sender)) { if (!SMTP.verify_address) { XplRWReadLockRelease (&ConfigLock); snprintf (Answer, sizeof (Answer), @@ -851,6 +1313,12 @@ HandleConnection (void *param) break; } } + if (!InternalRelayConsume(Client, 0, 1, 0)) { + ConnWrite(Client->client.conn, + "451 4.7.1 Internal relay recipient rate exceeded\r\n", + strlen("451 4.7.1 Internal relay recipient rate exceeded\r\n")); + goto QuitRcpt; + } NMAPSendCommand (Client->nmap.conn, Answer, strlen (Answer)); if (NMAPReadResponse (Client->nmap.conn, Reply, sizeof (Reply), TRUE) != 1000) { ConnWrite (Client->client.conn, MSG451INTERNALERR, @@ -880,11 +1348,18 @@ HandleConnection (void *param) char temp, *name; char *Envid = NULL; char *more; + char normalizedSender[MAXEMAILNAMESIZE + 1]; if (SMTP.require_auth && !IsTrusted) { ConnWrite (Client->client.conn, MSG553SPAMBLOCK, MSG553SPAMBLOCK_LEN); break; } + if (Client->Submission && SMTP.submission_require_auth && !IsAuthed) { + ConnWrite(Client->client.conn, + "530 5.7.0 Authentication required\r\n", + strlen("530 5.7.0 Authentication required\r\n")); + break; + } /* rfc 2821 */ if (!Client->State || Client->State >= STATE_FROM) { @@ -1088,7 +1563,8 @@ HandleConnection (void *param) ptr++; size = atol (ptr); XplRWReadLockAcquire (&ConfigLock); - if (SMTP.message_limit > 0 && size > SMTP.message_limit) { + if (SMTP.message_limit > 0 && + size > (unsigned long)SMTP.message_limit) { XplRWReadLockRelease (&ConfigLock); ConnWrite (Client->client.conn, MSG552MSGTOOBIG, MSG552MSGTOOBIG_LEN); @@ -1109,6 +1585,23 @@ HandleConnection (void *param) more++; } + if (Client->InternalRelay && + NormalizeInternalSender(name, Client->InternalDevice, normalizedSender, + sizeof(normalizedSender))) { + Log(LOG_INFO, "Normalized internal sender %s to %s for %s", + name, normalizedSender, LOGIP(Client->client.conn->socketAddress)); + name = normalizedSender; + } + if (Client->Submission && + !SubmissionSenderAllowed(Client->AuthFrom, name)) { + Log(LOG_WARN, "Submission user %s attempted sender %s from %s", + Client->AuthFrom != NULL ? Client->AuthFrom : "", name, + LOGIP(Client->client.conn->socketAddress)); + ConnWrite(Client->client.conn, + "553 5.7.1 Sender address not permitted\r\n", + strlen("553 5.7.1 Sender address not permitted\r\n")); + goto QuitMail; + } ReplyInt = strlen (name); if (ReplyInt > MAXEMAILNAMESIZE) { ConnWrite (Client->client.conn, MSG501GOAWAY, MSG501GOAWAY_LEN); @@ -1131,6 +1624,30 @@ HandleConnection (void *param) break; } + Client->HasSPFResult = FALSE; + XplRWReadLockAcquire(&ConfigLock); + { + BOOL verifySPF = SMTP.spf_verify; + XplRWReadLockRelease(&ConfigLock); + if (verifySPF) { + char clientAddress[INET_ADDRSTRLEN]; + BongoMailAuthStatus spfStatus; + + if (inet_ntop(AF_INET, + &Client->client.conn->socketAddress.sin_addr, + clientAddress, sizeof(clientAddress)) != NULL) { + spfStatus = BongoMailAuthCheckSPF( + BongoGlobals.hostname, clientAddress, + Client->RemoteHost, + name[0] != '\0' ? name : "", + &Client->SPFResult); + Client->HasSPFResult = + spfStatus == BONGO_MAILAUTH_OK || + spfStatus == BONGO_MAILAUTH_PROTOCOL_ERROR; + } + } + } + if (size > 0) { NMAPSendCommand (Client->nmap.conn, "QDSPC\r\n", 7); @@ -1260,15 +1777,38 @@ HandleConnection (void *param) TimeBuf); NMAPSendCommand(Client->nmap.conn, Answer, strlen(Answer)); + if (Client->HasSPFResult) { + snprintf(Answer, sizeof(Answer), + "Authentication-Results: %s; spf=%s smtp.mailfrom=%s\r\n", + BongoGlobals.hostname, + SPFResultName(Client->SPFResult.result), + (Client->From != NULL && Client->From[0] != '\0') + ? Client->From : "<>"); + NMAPSendCommand(Client->nmap.conn, Answer, strlen(Answer)); + } + /* read all the data to the nmap connection. this function * takes into account the single . terminating the connection */ - if (ConnReadToConnUntilEOS(Client->client.conn, Client->nmap.conn) < 0) { + count = ConnReadToConnUntilEOS(Client->client.conn, Client->nmap.conn); + if (count < 0) { ConnWrite(Client->client.conn, MSG451INTERNALERR, MSG451INTERNALERR_LEN); return(EndClientConnection(Client)); } + BReceived = (unsigned long) count; + if (!InternalRelayConsume(Client, 1, 0, BReceived)) { + NMAPSendCommand(Client->nmap.conn, "QABRT\r\n", 7); + NMAPReadResponse(Client->nmap.conn, Reply, sizeof(Reply), TRUE); + ConnWrite(Client->client.conn, + "451 4.7.1 Internal relay message rate exceeded\r\n", + strlen("451 4.7.1 Internal relay message rate exceeded\r\n")); + Client->State = STATE_HELO; + Client->RecipCount = 0; + break; + } XplRWReadLockAcquire (&ConfigLock); - if (SMTP.message_limit > 0 && BReceived > SMTP.message_limit) { /* Message over limit */ + if (SMTP.message_limit > 0 && + BReceived > (unsigned long)SMTP.message_limit) { /* Message over limit */ XplRWReadLockRelease (&ConfigLock); if ((NMAPSendCommand (Client->nmap.conn, "QABRT\r\n", 7) < 0) || NMAPReadResponse (Client->nmap.conn, Reply, sizeof (Reply), TRUE) != 1000) { @@ -1282,7 +1822,7 @@ HandleConnection (void *param) XplRWReadLockRelease (&ConfigLock); if ((NullSender == FALSE) - || (Client->RecipCount < SMTP.max_null_sender)) { + || (Client->RecipCount < (unsigned int)SMTP.max_null_sender)) { if (NMAPSendCommand (Client->nmap.conn, "QRUN\r\n", 6)<0) { ConnWrite (Client->client.conn, MSG451INTERNALERR, MSG451INTERNALERR_LEN); @@ -1351,13 +1891,14 @@ HandleConnection (void *param) XplRWReadLockAcquire (&ConfigLock); if (SMTP.message_limit > 0) { snprintf (Answer, sizeof (Answer), - "250-%s %s\r\n%s%s%s%s %lu\r\n", + "250-%s %s\r\n%s%s%s%s %d\r\n", BongoGlobals.hostname, MSG250HELLO, SMTP.accept_etrn ? MSG250ETRN : "", - (SMTP.allow_client_ssl + ((SMTP.allow_client_ssl || Client->InternalRelay || Client->Submission) + && (Client->InternalRelay ? InternalSSLContext != NULL : TRUE) && !Client->client.conn->ssl.enable) ? MSG250TLS : "", - (AllowAuth == - TRUE) ? MSG250AUTH : "", + (AllowAuth == TRUE && + Client->client.conn->ssl.enable) ? MSG250AUTH : "", MSG250EHLO, SMTP.message_limit); } else { @@ -1365,16 +1906,17 @@ HandleConnection (void *param) "250-%s %s\r\n%s%s%s%s\r\n", BongoGlobals.hostname, MSG250HELLO, SMTP.accept_etrn ? MSG250ETRN : "", - (SMTP.allow_client_ssl + ((SMTP.allow_client_ssl || Client->InternalRelay || Client->Submission) + && (Client->InternalRelay ? InternalSSLContext != NULL : TRUE) && !Client->client.conn->ssl.enable) ? MSG250TLS : "", - (AllowAuth == - TRUE) ? MSG250AUTH : "", + (AllowAuth == TRUE && + Client->client.conn->ssl.enable) ? MSG250AUTH : "", MSG250EHLO); } XplRWReadLockRelease (&ConfigLock); ConnWrite (Client->client.conn, Answer, strlen (Answer)); - strncpy (Client->RemoteHost, Client->Command + 5, - sizeof (Client->RemoteHost)); + snprintf(Client->RemoteHost, sizeof(Client->RemoteHost), + "%s", Client->Command + 5); Client->State = STATE_HELO; NullSender = FALSE; @@ -1662,13 +2204,11 @@ DeliverSMTPMessage (ConnectionStruct * Client, char *Sender, char Reply[1024]; char *ptr, *EnvID = NULL; int status; - long Size, len, MessageSize; + long Size, len; unsigned int i; BOOL EscapedState = FALSE; unsigned long LastNMAPContact; char TimeBuf[80]; - BOOL ehlo=TRUE; - BOOL tls=FALSE; status = GetAnswer (Client, Reply, sizeof (Reply)); HandleFailure (220); @@ -1688,7 +2228,6 @@ DeliverSMTPMessage (ConnectionStruct * Client, char *Sender, ConnFlush(Client->remotesmtp.conn); status = GetAnswer (Client, Reply, sizeof (Reply)); HandleFailure (250); - ehlo=FALSE; } else { /* The other server supports ESMTP */ @@ -1711,7 +2250,6 @@ DeliverSMTPMessage (ConnectionStruct * Client, char *Sender, } ConnFlush(Client->remotesmtp.conn); status = GetAnswer (Client, Reply, sizeof (Reply)); - tls=TRUE; } } @@ -1755,8 +2293,7 @@ DeliverSMTPMessage (ConnectionStruct * Client, char *Sender, } /* Keep this close to the above code; we rely on Reply not being overwritten */ - snprintf (Answer, sizeof (Answer), "MAIL FROM:<%s>", Reply); - if (ConnWrite(Client->remotesmtp.conn, Answer, strlen(Answer)) < 1) { + if (ConnWriteF(Client->remotesmtp.conn, "MAIL FROM:<%s>", Reply) < 1) { DELIVER_ERROR (DELIVER_TRY_LATER); } @@ -1834,7 +2371,7 @@ DeliverSMTPMessage (ConnectionStruct * Client, char *Sender, } if (Extensions & EXT_DSN) { - if (Recips[i].ORecip) { + if (Recips[i].ORecip[0] != '\0') { if (XplStrNCaseCmp (Recips[i].ORecip, "rfc822;", 7) == 0) { /* Add "rfc822;" if not already present */ snprintf (Answer, sizeof (Answer), " ORCPT=%s", Recips[i].ORecip); @@ -1951,7 +2488,7 @@ DeliverSMTPMessage (ConnectionStruct * Client, char *Sender, if (status == 550) { Recips[i].Result = DELIVER_USER_UNKNOWN; if ((unsigned long) ResultLen > strlen (Reply)) { - snprintf (Result, sizeof (Result), + snprintf (Result, ResultLen, ">>> RCPT TO: <%s>\\r\\n<<< %d %s", Recips[i].To, status, Reply); } @@ -1960,7 +2497,7 @@ DeliverSMTPMessage (ConnectionStruct * Client, char *Sender, } continue; } - else if ((status == 501) && Recips[i].ORecip + else if ((status == 501) && Recips[i].ORecip[0] != '\0' && (Extensions & EXT_DSN)) { /* workaround for smtp implimentations that do not do ORCPT: properly */ Extensions ^= EXT_DSN; @@ -2005,7 +2542,7 @@ DeliverSMTPMessage (ConnectionStruct * Client, char *Sender, if (status == 550) { Recips[i].Result = DELIVER_USER_UNKNOWN; if ((unsigned long) ResultLen > strlen (Reply)) { /* FIXME? This is not enough to prevent overwrites, but the buffer is really big... */ - snprintf (Result, sizeof (Result), + snprintf (Result, ResultLen, ">>> RCPT TO: <%s>\\r\\n<<< %d %s", Recips[i].To, status, Reply); } @@ -2057,8 +2594,6 @@ DeliverSMTPMessage (ConnectionStruct * Client, char *Sender, DELIVER_ERROR (DELIVER_FAILURE); } - MessageSize = Size; - /* Sending message to remote system; must escape any dots on a line */ while (Size > 0) { if ((unsigned int)Size < sizeof (Answer)) { @@ -2135,7 +2670,7 @@ DeliverRemoteMessage (ConnectionStruct * Client, char *Sender, conn->socketAddress.sin_addr.s_addr = inet_addr(SMTP.relay_host); conn->socketAddress.sin_family = AF_INET; conn->socketAddress.sin_port = htons (25); // TODO: configurable? - if (conn->socketAddress.sin_addr.s_addr == -1) { + if (conn->socketAddress.sin_addr.s_addr == INADDR_NONE) { // TODO: Resolve host? Implies a resolve loop for A recs. tho. DELIVER_ERROR_NO_RETURN(DELIVER_TRY_LATER); } else { @@ -2254,10 +2789,11 @@ finish: } int -RewriteAddress(Connection * conn, char *Source, char *Target, unsigned int TargetSize) +RewriteAddress(Connection * conn, const char *Source, char *Target, unsigned int TargetSize) { char WorkSpace[1024]; char *Src, *Dst; + const char *Input; unsigned int i; int RetVal = MAIL_BOGUS; @@ -2280,15 +2816,15 @@ RewriteAddress(Connection * conn, char *Source, char *Target, unsigned int Targe } /** remove illegal chars **/ - Src = Source; + Input = Source; Dst = WorkSpace; - while (*Src) { - if (!isspace (*Src) && *Src != '"') { - *(Dst++) = *(Src++); + while (*Input) { + if (!isspace ((unsigned char) *Input) && *Input != '"') { + *(Dst++) = *(Input++); } else { - Src++; + Input++; } } *Dst = '\0'; @@ -2453,15 +2989,15 @@ RewriteAddress(Connection * conn, char *Source, char *Target, unsigned int Targe static int RecipCompare (const RecipStruct * Recip1, const RecipStruct * Recip2) { - char *ptr1, *ptr2; + const char *ptr1, *ptr2; char char1, char2; if ((!(Recip1->To[0])) || (!(Recip2->To[0]))) { return (-1); } - ptr1 = strchr (Recip1->To, '@') + 1; /* We already made sure the @s exist */ - ptr2 = strchr (Recip2->To, '@') + 1; /* when we were building the list */ + ptr1 = strchr ((const char *) Recip1->To, '@') + 1; /* The @s were validated earlier. */ + ptr2 = strchr ((const char *) Recip2->To, '@') + 1; while (*ptr1 && *ptr2 && *ptr1 != ' ' && *ptr2 != ' ') { char1 = *(ptr1++) & 0xdf; char2 = *(ptr2++) & 0xdf; @@ -2586,7 +3122,7 @@ ProcessRemoteEntry (ConnectionStruct * Client, unsigned long Size, int Lines) Local = TRUE; j = 0; } - if (Recips[NumRecips].ORecip) { + if (Recips[NumRecips].ORecip[0] != '\0') { rc = snprintf (Reply, sizeof (Reply), "QSTOR LOCAL %s %s %lu\r\n", Recips[NumRecips].To, @@ -2661,7 +3197,7 @@ ProcessRemoteEntry (ConnectionStruct * Client, unsigned long Size, int Lines) case DELIVER_TRY_LATER:{ len = snprintf (Reply, sizeof (Reply), "QMOD RAW R%s %s %lu\r\n", Recips[j].To, - Recips[j].ORecip ? Recips[j]. + Recips[j].ORecip[0] != '\0' ? Recips[j]. ORecip : Recips[j].To, Recips[j].Flags); NMAPSendCommand (Client->client.conn, Reply, len); } @@ -2677,7 +3213,7 @@ ProcessRemoteEntry (ConnectionStruct * Client, unsigned long Size, int Lines) len = snprintf (Reply, sizeof (Reply), "QRTS %s %s %lu %d %s\r\n", Recips[j].To, - Recips[j].ORecip ? Recips[j]. + Recips[j].ORecip[0] != '\0' ? Recips[j]. ORecip : Recips[j].To, Recips[j].Flags, Recips[j].Result, Result); @@ -2685,7 +3221,7 @@ ProcessRemoteEntry (ConnectionStruct * Client, unsigned long Size, int Lines) len = snprintf (Reply, sizeof (Reply), "QRTS %s %s %lu %d\r\n", Recips[j].To, - Recips[j].ORecip ? Recips[j]. + Recips[j].ORecip[0] != '\0' ? Recips[j]. ORecip : Recips[j].To, Recips[j].Flags, Recips[j].Result); } @@ -2760,7 +3296,9 @@ RelayRemoteEntry (ConnectionStruct * client, unsigned long size, int lines) case QUEUE_CALENDAR_LOCAL:{ /* All calendar messages should be delivered locally. */ - NMAPSendCommand (client->client.conn, reply, snprintf (reply, BUFSIZE, "QMOD RAW %s\r\n", reply)); + NMAPSendCommand (client->client.conn, result, + snprintf (result, sizeof(result), + "QMOD RAW %s\r\n", reply)); continue; } @@ -2769,7 +3307,9 @@ RelayRemoteEntry (ConnectionStruct * client, unsigned long size, int lines) if (msgFlags & (MSG_FLAG_SOURCE_EXTERNAL | MSG_FLAG_CALENDAR_OBJECT)) { /* Calendar messages should be delivered locally. Externally received messages (relayed here) should be delivered locally. */ - NMAPSendCommand (client->client.conn, reply, snprintf (reply, BUFSIZE, "QMOD RAW %s\r\n", reply)); + NMAPSendCommand (client->client.conn, result, + snprintf (result, sizeof(result), + "QMOD RAW %s\r\n", reply)); continue; } @@ -2779,7 +3319,7 @@ RelayRemoteEntry (ConnectionStruct * client, unsigned long size, int lines) continue; } - /* Fall through */ + __attribute__((fallthrough)); /* All internally generated messages should be relayed. */ } @@ -2860,7 +3400,7 @@ RelayRemoteEntry (ConnectionStruct * client, unsigned long size, int lines) j = 0; } - if (recips[recipCount].ORecip) { + if (recips[recipCount].ORecip[0] != '\0') { NMAPSendCommand (client->client.conn, reply, snprintf (reply, sizeof (reply), @@ -2976,7 +3516,7 @@ RelayRemoteEntry (ConnectionStruct * client, unsigned long size, int lines) snprintf (reply, sizeof (reply), "QMOD RAW R%s %s %lu\r\n", recips[j].To, - recips[j].ORecip ? recips[j]. + recips[j].ORecip[0] != '\0' ? recips[j]. ORecip : recips[j].To, recips[j].Flags)); break; @@ -2994,7 +3534,7 @@ RelayRemoteEntry (ConnectionStruct * client, unsigned long size, int lines) "QRTS %s %s %lu %d %s\r\n", recips[j].To, recips[j]. - ORecip ? recips[j]. + ORecip[0] != '\0' ? recips[j]. ORecip : recips[j].To, recips[j].Flags, recips[j].Result, @@ -3006,7 +3546,7 @@ RelayRemoteEntry (ConnectionStruct * client, unsigned long size, int lines) "QRTS %s %s %lu %d\r\n", recips[j].To, recips[j]. - ORecip ? recips[j]. + ORecip[0] != '\0' ? recips[j]. ORecip : recips[j].To, recips[j].Flags, recips[j].Result)); @@ -3092,6 +3632,8 @@ RelayLocalEntry (ConnectionStruct * client, unsigned long size, char reply[BUFSIZE + 1]; BOOL Local = FALSE; + UNUSED_PARAMETER(lines); + envelope = MemMalloc (size + 1); next = envelope; while ((rc = NMAPReadResponse (client->client.conn, buffer, sizeof (buffer) - 1, FALSE)) != 6021) { @@ -3234,7 +3776,7 @@ RelayLocalEntry (ConnectionStruct * client, unsigned long size, case QUEUE_CALENDAR_LOCAL:{ msgFlags |= MSG_FLAG_CALENDAR_OBJECT; - /* Fall through */ + __attribute__((fallthrough)); } case QUEUE_RECIP_LOCAL: @@ -3292,6 +3834,8 @@ ProcessLocalEntry (ConnectionStruct * Client, unsigned long Size, char *Envelope = MemMalloc (Size + 1); char *NextLine = Envelope; + UNUSED_PARAMETER(Lines); + while ((rc = NMAPReadResponse (Client->client.conn, Line, sizeof (Line), FALSE)) != 6021) { rc = strlen (Line); memcpy (NextLine, Line, rc); @@ -3501,7 +4045,7 @@ HandleQueueConnection (void *ClientIn) return; } -static void +static void __attribute__((unused)) QueueServerStartup (void *ignored) { Connection *conn; @@ -3510,6 +4054,8 @@ QueueServerStartup (void *ignored) XplThreadID id = 0; BOOL qd = FALSE, qo = FALSE; + UNUSED_PARAMETER(ignored); + XplRenameThread (XplGetThreadID (), "SMTP NMAP Q Monitor"); SMTPQServerConnection = ConnAlloc(FALSE); @@ -3592,6 +4138,16 @@ SMTPShutdownSigHandler (int sigtype) ConnFree(SMTPServerConnection); SMTPServerConnection = NULL; } + if (SMTPInternalServerConnection) { + ConnClose(SMTPInternalServerConnection); + ConnFree(SMTPInternalServerConnection); + SMTPInternalServerConnection = NULL; + } + if (SMTPSubmissionServerConnection) { + ConnClose(SMTPSubmissionServerConnection); + ConnFree(SMTPSubmissionServerConnection); + SMTPSubmissionServerConnection = NULL; + } XplSignalLocalSemaphore (SMTPShutdownSemaphore); } else if (signaled && (sigtype == SIGTERM || sigtype == SIGINT)) { @@ -3650,7 +4206,8 @@ ServerSocketSSLInit (void) } SMTPServerConnectionSSL->socketAddress.sin_family = AF_INET; - SMTPServerConnectionSSL->socketAddress.sin_port = htons (SMTP.port_ssl); + SMTPServerConnectionSSL->socketAddress.sin_port = htons( + SMTP.submission_enabled ? SMTP.submission_port_ssl : SMTP.port_ssl); SMTPServerConnectionSSL->socketAddress.sin_addr.s_addr = MsgGetAgentBindIPAddress (); /* Get root privs back for the bind. It's ok if this fails - @@ -3673,6 +4230,182 @@ ServerSocketSSLInit (void) return 0; } +static int +SubmissionServerSocketInit(void) +{ + int ccode; + SMTPSubmissionServerConnection = ConnAlloc(FALSE); + if (SMTPSubmissionServerConnection == NULL) return -1; + SMTPSubmissionServerConnection->socketAddress.sin_family = AF_INET; + SMTPSubmissionServerConnection->socketAddress.sin_port = htons(SMTP.submission_port); + SMTPSubmissionServerConnection->socketAddress.sin_addr.s_addr = MsgGetAgentBindIPAddress(); + XplSetEffectiveUserId(0); + SMTPSubmissionServerConnection->socket = ConnServerSocket(SMTPSubmissionServerConnection, 256); + if (XplSetEffectiveUser(MsgGetUnprivilegedUser()) < 0) return -1; + if (SMTPSubmissionServerConnection->socket < 0) { + ccode = SMTPSubmissionServerConnection->socket; + ConnFree(SMTPSubmissionServerConnection); + SMTPSubmissionServerConnection = NULL; + return ccode; + } + return 0; +} + +static void +SMTPSubmissionServer(void *ignored) +{ + Connection *conn; + ConnectionStruct *client; + XplThreadID id; + int ccode; + (void) ignored; + XplRenameThread(XplGetThreadID(), "SMTP Submission Server"); + while (!Exiting) { + if (ConnAccept(SMTPSubmissionServerConnection, &conn) == -1) { + if (!Exiting && errno == EINTR) continue; + break; + } + if (SMTPReceiverStopped || XplSafeRead(SMTPConnThreads) >= SMTP.max_thread_load) { + ConnSend(conn, MSG451NOCONNECTIONS, MSG451NOCONNECTIONS_LEN); + ConnClose(conn); + ConnFree(conn); + continue; + } + client = GetSMTPConnection(); + if (client == NULL) { + ConnSend(conn, MSG500NOMEMORY, MSG500NOMEMORY_LEN); + ConnClose(conn); + ConnFree(conn); + continue; + } + client->client.conn = conn; + client->Submission = TRUE; + XplBeginCountedThread(&id, HandleConnection, STACKSIZE_S, client, ccode, SMTPConnThreads); + if (ccode == 0) continue; + ReturnSMTPConnection(client); + ConnClose(conn); + ConnFree(conn); + } + XplSafeDecrement(SMTPServerThreads); + Log(LOG_DEBUG, "SMTP submission listening thread terminated"); +} + +static int +InternalServerSocketInit(void) +{ + int ccode; + + SMTPInternalServerConnection = ConnAlloc(FALSE); + if (SMTPInternalServerConnection == NULL) { + return -1; + } + SMTPInternalServerConnection->socketAddress.sin_family = AF_INET; + SMTPInternalServerConnection->socketAddress.sin_port = htons(SMTP.internal_relay_port); + if (inet_pton(AF_INET, SMTP.internal_relay_bind_address, + &SMTPInternalServerConnection->socketAddress.sin_addr) != 1) { + Log(LOG_ERROR, "Invalid internal SMTP bind address: %s", + SMTP.internal_relay_bind_address); + ConnFree(SMTPInternalServerConnection); + SMTPInternalServerConnection = NULL; + return -1; + } + XplSetEffectiveUserId(0); + SMTPInternalServerConnection->socket = ConnServerSocket(SMTPInternalServerConnection, 128); + if (XplSetEffectiveUser(MsgGetUnprivilegedUser()) < 0) { + return -1; + } + if (SMTPInternalServerConnection->socket < 0) { + ccode = SMTPInternalServerConnection->socket; + Log(LOG_ERROR, "Could not bind internal SMTP to %s:%d", + SMTP.internal_relay_bind_address, SMTP.internal_relay_port); + ConnFree(SMTPInternalServerConnection); + SMTPInternalServerConnection = NULL; + return ccode; + } + return 0; +} + +static void +SMTPInternalServer(void *ignored) +{ + Connection *conn; + ConnectionStruct *client; + XplThreadID id; + int ccode; + + (void) ignored; + XplRenameThread(XplGetThreadID(), "SMTP Internal Server"); + while (!Exiting) { + if (ConnAccept(SMTPInternalServerConnection, &conn) == -1) { + if (!Exiting && errno == EINTR) continue; + break; + } + if (!InternalRelayAddressAllowed(conn->socketAddress.sin_addr)) { + Log(LOG_WARN, "Rejected internal SMTP connection from %s", LOGIP(conn->socketAddress)); + ConnSend(conn, "554 5.7.1 Source address not permitted\r\n", 41); + ConnClose(conn); + ConnFree(conn); + continue; + } + if (SMTPReceiverStopped || XplSafeRead(SMTPConnThreads) >= SMTP.max_thread_load) { + ConnSend(conn, MSG451NOCONNECTIONS, MSG451NOCONNECTIONS_LEN); + ConnClose(conn); + ConnFree(conn); + continue; + } + client = GetSMTPConnection(); + if (client == NULL) { + ConnSend(conn, MSG500NOMEMORY, MSG500NOMEMORY_LEN); + ConnClose(conn); + ConnFree(conn); + continue; + } + client->client.conn = conn; + client->InternalRelay = TRUE; + XplMutexLock(InternalRelayRateLock); + client->InternalRate = g_hash_table_lookup(InternalRelayRates, + &conn->socketAddress.sin_addr.s_addr); + if (client->InternalRate == NULL) { + uint32_t *key = g_new(uint32_t, 1); + client->InternalRate = g_new0(InternalRelayRate, 1); + if (key != NULL && client->InternalRate != NULL) { + *key = conn->socketAddress.sin_addr.s_addr; + client->InternalRate->window = time(NULL); + g_hash_table_insert(InternalRelayRates, key, client->InternalRate); + } else { + g_free(key); + g_free(client->InternalRate); + client->InternalRate = NULL; + } + } + if (client->InternalRate == NULL || + client->InternalRate->connections >= + (unsigned int) SMTP.internal_relay_connections_per_ip) { + XplMutexUnlock(InternalRelayRateLock); + ConnSend(conn, "451 4.7.1 Too many internal relay connections\r\n", + strlen("451 4.7.1 Too many internal relay connections\r\n")); + ReturnSMTPConnection(client); + ConnClose(conn); + ConnFree(conn); + continue; + } + client->InternalRate->connections++; + XplMutexUnlock(InternalRelayRateLock); + XplBeginCountedThread(&id, HandleConnection, STACKSIZE_S, client, ccode, SMTPConnThreads); + if (ccode == 0) continue; + XplMutexLock(InternalRelayRateLock); + if (client->InternalRate != NULL && client->InternalRate->connections > 0) { + client->InternalRate->connections--; + } + XplMutexUnlock(InternalRelayRateLock); + ReturnSMTPConnection(client); + ConnClose(conn); + ConnFree(conn); + } + XplSafeDecrement(SMTPServerThreads); + Log(LOG_DEBUG, "Internal SMTP listening thread terminated"); +} + static void SMTPServer (void *ignored) { @@ -3683,6 +4416,8 @@ SMTPServer (void *ignored) ConnectionStruct *client; XplThreadID id; + UNUSED_PARAMETER(ignored); + XplSafeIncrement (SMTPServerThreads); XplRenameThread (XplGetThreadID(), "SMTP Server"); @@ -3760,6 +4495,7 @@ SMTPServer (void *ignored) Exiting = TRUE; oldTGID = XplSetThreadGroupID (TGid); + UNUSED_PARAMETER(oldTGID); Log(LOG_DEBUG, "Closing server socket"); @@ -3773,6 +4509,16 @@ SMTPServer (void *ignored) ConnFree(SMTPServerConnectionSSL); SMTPServerConnectionSSL = NULL; } + if (SMTPInternalServerConnection) { + ConnClose(SMTPInternalServerConnection); + ConnFree(SMTPInternalServerConnection); + SMTPInternalServerConnection = NULL; + } + if (SMTPSubmissionServerConnection) { + ConnClose(SMTPSubmissionServerConnection); + ConnFree(SMTPSubmissionServerConnection); + SMTPSubmissionServerConnection = NULL; + } if (SMTPQServerConnection) { ConnClose(SMTPQServerConnection); @@ -3816,6 +4562,14 @@ SMTPServer (void *ignored) ConnSSLContextFree(SSLContext); SSLContext = NULL; } + if (InternalSSLContext && InternalSSLContext != SSLContext) { + ConnSSLContextFree(InternalSSLContext); + InternalSSLContext = NULL; + } + + g_hash_table_destroy(InternalRelayRates); + InternalRelayRates = NULL; + XplMutexDestroy(InternalRelayRateLock); LogShutdown (); @@ -3849,6 +4603,8 @@ SMTPSSLServer (void *ignored) ConnectionStruct *client; XplThreadID id = 0; + UNUSED_PARAMETER(ignored); + XplRenameThread (XplGetThreadID (), "SMTP SSL Server"); while(!Exiting) { @@ -3860,6 +4616,7 @@ SMTPSSLServer (void *ignored) client = GetSMTPConnection(); if (client) { client->client.conn = conn; + client->Submission = SMTP.submission_enabled; XplBeginCountedThread (&id, HandleConnection, STACKSIZE_S, client, ccode, SMTPConnThreads); if (ccode == 0) { continue; @@ -3974,6 +4731,82 @@ ReadConfiguration (void) return (TRUE); } +static BOOL +ValidateListenerConfiguration(void) +{ + unsigned int index; + if (SMTP.submission_enabled && + (SMTP.submission_port < 1 || SMTP.submission_port > 65535 || + SMTP.submission_port_ssl < 1 || SMTP.submission_port_ssl > 65535)) { + Log(LOG_ERROR, "SMTP submission ports must be between 1 and 65535"); + return FALSE; + } + if (SMTP.submission_enabled && (!SMTP.allow_auth || !SMTP.submission_require_auth)) { + Log(LOG_ERROR, "SMTP submission must have authentication enabled and required"); + return FALSE; + } + if (SMTP.submission_enabled && + (SMTP.submission_port == SMTP.port || + SMTP.submission_port_ssl == SMTP.port || + SMTP.submission_port == SMTP.submission_port_ssl)) { + Log(LOG_ERROR, "SMTP relay and submission ports must be distinct"); + return FALSE; + } + if (SMTP.internal_relay_enabled) { + if (SMTP.internal_relay_port < 1 || SMTP.internal_relay_port > 65535) { + Log(LOG_ERROR, "Internal SMTP relay port must be between 1 and 65535"); + return FALSE; + } + if (SMTP.internal_relay_port == SMTP.port || + (SMTP.submission_enabled && + (SMTP.internal_relay_port == SMTP.submission_port || + SMTP.internal_relay_port == SMTP.submission_port_ssl))) { + Log(LOG_ERROR, "Internal SMTP relay port conflicts with another SMTP listener"); + return FALSE; + } + if (SMTP.internal_relay_bind_address == NULL || + *SMTP.internal_relay_bind_address == '\0' || + SMTP.internal_relay_networks == NULL || + SMTP.internal_relay_networks->len == 0) { + Log(LOG_ERROR, "Internal SMTP relay requires a bind address and at least one allowed source network"); + return FALSE; + } + if (SMTP.internal_relay_messages_per_minute < 1 || + SMTP.internal_relay_recipients_per_minute < 1 || + SMTP.internal_relay_bytes_per_minute < 1 || + SMTP.internal_relay_connections_per_ip < 1) { + Log(LOG_ERROR, "Internal SMTP relay limits must be greater than zero"); + return FALSE; + } + if (SMTP.internal_relay_domain == NULL || + *SMTP.internal_relay_domain == '\0') { + Log(LOG_ERROR, "Internal SMTP relay requires a normalization domain"); + return FALSE; + } + for (index = 0; SMTP.internal_relay_device_mappings != NULL && + index < SMTP.internal_relay_device_mappings->len; index++) { + const char *mapping = g_array_index(SMTP.internal_relay_device_mappings, + char *, index); + const char *equals = mapping != NULL ? strchr(mapping, '=') : NULL; + char ip[INET_ADDRSTRLEN]; + struct in_addr parsed; + size_t length = equals != NULL ? (size_t) (equals - mapping) : 0; + if (length == 0 || length >= sizeof(ip) || !ValidDeviceName(equals + 1)) { + Log(LOG_ERROR, "Invalid internal SMTP device mapping: %s", + mapping != NULL ? mapping : "(null)"); + return FALSE; + } + memcpy(ip, mapping, length); + ip[length] = '\0'; + if (inet_pton(AF_INET, ip, &parsed) != 1) { + Log(LOG_ERROR, "Invalid IP address in internal SMTP device mapping: %s", mapping); + return FALSE; + } + } + } + return TRUE; +} + XplServiceCode (SMTPShutdownSigHandler) int XplServiceMain (int argc, char *argv[]) @@ -3989,6 +4822,13 @@ int XplServiceMain (int argc, char *argv[]) } XplInit(); + XplMutexInit(InternalRelayRateLock); + InternalRelayRates = g_hash_table_new_full(g_int_hash, g_int_equal, g_free, g_free); + if (InternalRelayRates == NULL) { + Log(LOG_ERROR, "Could not allocate internal SMTP rate-limit table"); + return 1; + } + XplSignalHandler (SMTPShutdownSigHandler); XplSafeWrite (SMTPServerThreads, 0); @@ -4037,6 +4877,9 @@ int XplServiceMain (int argc, char *argv[]) Log(LOG_ERROR, "Unable to read Global configration from the store."); exit(-1); } + if (!ValidateListenerConfiguration()) { + return 1; + } ReadConfiguration (); if (ServerSocketInit () < 0) { @@ -4046,9 +4889,13 @@ int XplServiceMain (int argc, char *argv[]) // XplBeginCountedThread (&ID, QueueServerStartup, STACKSIZE_Q, NULL, ccode, SMTPServerThreads); - if (SMTP.allow_client_ssl) { + if (SMTP.allow_client_ssl || SMTP.submission_enabled) { if (!ServerSocketSSLInit()) { ConnSSLConfiguration sslconfig; + memset(&sslconfig, 0, sizeof(sslconfig)); + if (SMTP.allow_legacy_tls) { + sslconfig.options |= SSL_ALLOW_LEGACY_PROTOCOLS; + } sslconfig.key.file = MsgGetFile(MSGAPI_FILE_PRIVKEY, NULL, 0); sslconfig.certificate.file = MsgGetFile(MSGAPI_FILE_PUBKEY, NULL, 0); sslconfig.key.type = GNUTLS_X509_FMT_PEM; @@ -4057,10 +4904,50 @@ int XplServiceMain (int argc, char *argv[]) if (SSLContext) { XplBeginCountedThread (&ID, SMTPSSLServer, STACKSIZE_S, NULL, ccode, SMTPServerThreads); } else { - SMTP.allow_client_ssl = FALSE; + if (!SMTP.submission_enabled) SMTP.allow_client_ssl = FALSE; } } else { - SMTP.allow_client_ssl = FALSE; + if (!SMTP.submission_enabled) SMTP.allow_client_ssl = FALSE; + } + } + + if (SMTP.submission_enabled) { + if (SSLContext == NULL) { + Log(LOG_ERROR, "SMTP submission requires a usable TLS certificate and key"); + return 1; + } + if (SubmissionServerSocketInit() < 0) { + Log(LOG_ERROR, "Can't open SMTP submission port %d", SMTP.submission_port); + return 1; + } + XplBeginCountedThread(&ID, SMTPSubmissionServer, STACKSIZE_S, NULL, ccode, + SMTPServerThreads); + if (ccode != 0) return 1; + } + + if (SMTP.internal_relay_enabled) { + ConnSSLConfiguration sslconfig; + + if (InternalServerSocketInit() < 0) { + Log(LOG_ERROR, "Can't open internal SMTP relay port"); + return 1; + } + memset(&sslconfig, 0, sizeof(sslconfig)); + if (SMTP.internal_relay_allow_legacy_tls) { + sslconfig.options |= SSL_ALLOW_LEGACY_PROTOCOLS; + } + sslconfig.key.file = MsgGetFile(MSGAPI_FILE_PRIVKEY, NULL, 0); + sslconfig.certificate.file = MsgGetFile(MSGAPI_FILE_PUBKEY, NULL, 0); + sslconfig.key.type = GNUTLS_X509_FMT_PEM; + InternalSSLContext = ConnSSLContextAlloc(&sslconfig); + if (InternalSSLContext == NULL) { + Log(LOG_WARN, "Internal SMTP STARTTLS disabled: TLS context could not be loaded"); + } + XplBeginCountedThread(&ID, SMTPInternalServer, STACKSIZE_S, NULL, ccode, + SMTPServerThreads); + if (ccode != 0) { + Log(LOG_ERROR, "Could not start internal SMTP listener thread"); + return 1; } } diff --git a/src/agents/smtp/smtpd.h b/src/agents/smtp/smtpd.h index 9bbbcb5..77d870e 100755 --- a/src/agents/smtp/smtpd.h +++ b/src/agents/smtp/smtpd.h @@ -75,7 +75,7 @@ #define MSG250ETRN "250-ETRN\r\n" #define MSG250TLS "250-STARTTLS\r\n" -#define MSG250AUTH "250-AUTH LOGIN\r\n250-AUTH=LOGIN\r\n" +#define MSG250AUTH "250-AUTH PLAIN LOGIN\r\n" #define MSG250EHLO "250-HELP\r\n250-EXPN\r\n250-PIPELINING\r\n250-8BITMIME\r\n250-DSN\r\n250 SIZE" #define MSG251NOMESSAGES "251 OK No messages waiting for you\r\n" @@ -176,6 +176,9 @@ #define MSG550TOOMANY "550 Too many recipients\r\n" #define MSG550TOOMANY_LEN 25 +#define MSG550SRSINVALID "550 Invalid or expired SRS recipient\r\n" +#define MSG550SRSINVALID_LEN 38 + #define MSG552MSGTOOBIG "552 Message size exceeds maximum allowed by this server\r\n" #define MSG552MSGTOOBIG_LEN 57 @@ -192,4 +195,3 @@ /* DO NOT ADD A CR/LF AT THE END OF THE FOLLOWING STRING - this is not a response code, but a fake error code for sending */ #define MSG422CONNERROR "422 Remote system unexpectedly closed the connection." #define MSG422CONNERROR_LEN 53 - diff --git a/src/agents/store/command-parsing.c b/src/agents/store/command-parsing.c index a38a47e..f950520 100644 --- a/src/agents/store/command-parsing.c +++ b/src/agents/store/command-parsing.c @@ -13,7 +13,7 @@ typedef struct { const StorePrivilege priv; - const char const *name; + const char *name; } StorePrivName; static const StorePrivName StorePrivilegeTable[] = { @@ -35,7 +35,7 @@ static const StorePrivName StorePrivilegeTable[] = { }; CCode -ParsePrivilege(StoreClient *client, const char const *token, StorePrivilege *priv) +ParsePrivilege(StoreClient *client, const char *token, StorePrivilege *priv) { StorePrivName const *priventry = StorePrivilegeTable; @@ -75,7 +75,7 @@ PrivilegeToString(const StorePrivilege priv, char const **out) } CCode -ParsePrincipal(StoreClient *client, const char const *token, StorePrincipalType *type) +ParsePrincipal(StoreClient *client, const char *token, StorePrincipalType *type) { if (client == NULL) return -1; @@ -186,7 +186,7 @@ ParseUnsignedLong(StoreClient *client, char *token, unsigned long *out) CCode -ParseInt(StoreClient *client, const char const *token, int *out) +ParseInt(StoreClient *client, const char *token, int *out) { long t; char *endp; @@ -293,6 +293,7 @@ ParseIcalDateTime(StoreClient *client, CCode ParseDateTime(StoreClient *client, char *token, BongoCalTime *timeOut) { + CCode ccode; int len; len = strlen(token); @@ -303,7 +304,6 @@ ParseDateTime(StoreClient *client, char *token, BongoCalTime *timeOut) return ParseIcalDateTime(client, token, timeOut); } else { unsigned long ulong=0; - CCode ccode; ccode = ParseUnsignedLong(client, token, &ulong); @@ -335,7 +335,6 @@ ParseDateTimeToUint64(StoreClient *client, char *token, uint64_t *timeOut) } else { /* If it's a long arg, just pass it through */ unsigned long ulong=0; - CCode ccode; ccode = ParseUnsignedLong(client, token, &ulong); diff --git a/src/agents/store/command-parsing.h b/src/agents/store/command-parsing.h index 0a440bd..3938220 100644 --- a/src/agents/store/command-parsing.h +++ b/src/agents/store/command-parsing.h @@ -10,7 +10,7 @@ CheckTokC(StoreClient *client, int n, int min, int max); CCode ParseGUID(StoreClient *client, char *token, uint64_t *out); CCode ParseUnsignedLong(StoreClient *client, char *token, unsigned long *out); -CCode ParseInt(StoreClient *client, const char const *token, int *out); +CCode ParseInt(StoreClient *client, const char *token, int *out); CCode ParseHexU32(StoreClient *client, char *token, uint32_t *out); CCode ParseStreamLength(StoreClient *client, char *token, int *out); int ParseRange(StoreClient *client, char *token, int *startOut, int *endOut); @@ -31,8 +31,8 @@ CCode ParseCreateDocument(StoreClient *client, char *token, uint64_t *outGuid, S CCode ParseDocType(StoreClient *client, char *token, StoreDocumentType *out); CCode ParsePropName(StoreClient *client, char *token, StorePropInfo *prop); CCode ParsePropList(StoreClient *client, char *token, StorePropInfo * props, int propcount, int *count, int requireLengths); -CCode ParsePrivilege(StoreClient *client, const char const *token, StorePrivilege *priv); -CCode ParsePrincipal(StoreClient *client, const char const *token, StorePrincipalType *type); +CCode ParsePrivilege(StoreClient *client, const char *token, StorePrivilege *priv); +CCode ParsePrincipal(StoreClient *client, const char *token, StorePrincipalType *type); CCode ParseHeaderList(StoreClient *client, char *token, StoreHeaderInfo *headers, int size, int *count); CCode ParseStoreName(StoreClient *client, char *token); CCode ParseUserName(StoreClient *client, char *token); diff --git a/src/agents/store/command.c b/src/agents/store/command.c index 984508a..6903871 100644 --- a/src/agents/store/command.c +++ b/src/agents/store/command.c @@ -75,7 +75,10 @@ CommandCmp(const void *cmda, const void *cmdb) return 0; } -#define CHECK_NOT_READONLY(x) { CCode ret = IsStoreReadonly((x)); if (ret != 0) return ret; } +#define CHECK_NOT_READONLY(x) do { \ + CCode readonly_code = IsStoreReadonly((x)); \ + if (readonly_code != 0) return readonly_code; \ +} while (0); static CCode IsStoreReadonly(StoreClient *client) @@ -260,7 +263,7 @@ StoreCommandLoop(StoreClient *client) if (XplStrNCaseCmp("AUTH", client->buffer, 4) == 0) { strcpy(command_message, "-> AUTH ....."); } else { - snprintf(command_message, 255, "-> %s", client->buffer); + snprintf(command_message, sizeof(command_message), "-> %.251s", client->buffer); } Ringlog(command_message); @@ -269,7 +272,8 @@ StoreCommandLoop(StoreClient *client) if (0 == n) { command = STORE_COMMAND_NULL; } else { - command = (StoreCommand) BongoHashtableGet(CommandTable, tokens[0]); + command = (StoreCommand) (intptr_t) + BongoHashtableGet(CommandTable, tokens[0]); } if (client->watch.collection.guid > 0) { @@ -1637,7 +1641,11 @@ StoreCommandCOPY(StoreClient *client, StoreObject *object, StoreObject *collecti newobject.collection_guid = collection->guid; newobject.time_created = newobject.time_modified = time(NULL); - StoreObjectFixUpFilename(collection, &newobject); + if (StoreObjectFixUpFilename(collection, &newobject) != 0) { + StoreObjectRemove(client, &newobject); + ccode = ConnWriteStr(client->conn, MSG4228CANTWRITEMBOX); + goto finish; + } StoreObjectSave(client, &newobject); @@ -1782,7 +1790,11 @@ StoreCommandCREATE(StoreClient *client, char *name, uint64_t guid) // check we can save this new object and if not, error out object.collection_guid = container.guid; - StoreObjectFixUpFilename(&container, &object); + if (StoreObjectFixUpFilename(&container, &object) != 0) { + StoreObjectRemove(client, &object); + LogicalLockRelease(client, &container, LLOCK_READWRITE, "StoreCommandCREATE"); + return ConnWriteStr(client->conn, MSG4228CANTWRITEMBOX); + } if (StoreObjectSave(client, &object)) { StoreObjectRemove(client, &object); @@ -2394,7 +2406,10 @@ StoreCommandMOVE(StoreClient *client, StoreObject *object, object->collection_guid = destination_collection->guid; if (newfilename != NULL) strncpy(object->filename, newfilename, MAX_FILE_NAME); - StoreObjectFixUpFilename(destination_collection, object); + if (StoreObjectFixUpFilename(destination_collection, object) != 0) { + ccode = ConnWriteStr(client->conn, MSG4228CANTWRITEMBOX); + goto finish; + } StoreObjectUpdateImapUID(client, object); if (STORE_DOCTYPE_MAIL == object->type) { @@ -2794,15 +2809,15 @@ StoreCommandREPAIR(StoreClient *client) switch (test) { case 1: // repaired - sprintf(reason, "2001 %s\r\n", object.filename); + snprintf(reason, sizeof(reason), "2001 %.192s\r\n", object.filename); break; case -1: // broken - sprintf(reason, "2011 %s\r\n", object.filename); + snprintf(reason, sizeof(reason), "2011 %.192s\r\n", object.filename); break; case -2: // removed - sprintf(reason, "2012 %s\r\n", object.filename); + snprintf(reason, sizeof(reason), "2012 %.192s\r\n", object.filename); break; default: break; @@ -3093,7 +3108,11 @@ StoreCommandWRITE(StoreClient *client, // place the new document in the right collection // we take the lock on the collection this late, newdocument.collection_guid = collection->guid; - StoreObjectFixUpFilename(collection, &newdocument); + if (StoreObjectFixUpFilename(collection, &newdocument) != 0) { + unlink(path); + LogicalLockRelease(client, collection, LLOCK_READWRITE, "StoreCommandWRITE"); + return ConnWriteStr(client->conn, MSG4224CANTWRITE); + } StoreObjectUpdateImapUID(client, &newdocument); if (StoreObjectSave(client, &newdocument)) { diff --git a/src/agents/store/contacts.c b/src/agents/store/contacts.c index d110dca..96e1a44 100644 --- a/src/agents/store/contacts.c +++ b/src/agents/store/contacts.c @@ -39,11 +39,17 @@ StoreProcessIncomingContact(StoreClient *client, StoreObject *contact, char *pat BongoJsonResult result; result = BongoJsonJPathGetString(node, "o:fullName/s", &name); - if (result != BONGO_JSON_OK) name = NULL; + if (result != BONGO_JSON_OK) { + result = BongoJsonJPathGetString(node, "o:fn/s", &name); + if (result != BONGO_JSON_OK) name = NULL; + } // FIXME: want to find the preferred address really. result = BongoJsonJPathGetString(node, "o:email/a:0/o:address/s", &email); - if (result != BONGO_JSON_OK) email = NULL; + if (result != BONGO_JSON_OK) { + result = BongoJsonJPathGetString(node, "o:email/a:0/o:value/s", &email); + if (result != BONGO_JSON_OK) email = NULL; + } if (name) SetDocProp(client, contact, "bongo.contact.name", name); if (email) SetDocProp(client, contact, "bongo.contact.email", email); diff --git a/src/agents/store/cookie.c b/src/agents/store/cookie.c index d6661c4..9568df0 100644 --- a/src/agents/store/cookie.c +++ b/src/agents/store/cookie.c @@ -21,6 +21,7 @@ OpenCookieDB(StoreClient *client) MsgSQLStatement *schema = NULL; MsgSQLHandle *cdb = MsgSQLOpen(path, &client->memstack, 3000); + if (cdb == NULL) return NULL; if (MsgSQLBeginTransaction(cdb)) return NULL; @@ -64,17 +65,14 @@ StoreCommandCOOKIENEW(StoreClient *client, uint64_t timeout) //MsgSQLStatement *ret; memset(&stmt, 0, sizeof(MsgSQLStatement)); - char token[50]; - xpl_hash_context context; + unsigned char random[32]; + char token[(sizeof(random) * 2) + 1]; MsgSQLHandle *db = NULL; - XplHashNew(&context, XPLHASH_MD5); - snprintf(token, sizeof(token) - 1, "%x%x", - (unsigned int) XplGetThreadID(), (unsigned int) time(NULL)); - XplHashWrite(&context, token, strlen(token)); - XplRandomData(token, 8); - XplHashWrite(&context, token, strlen(token)); - XplHashFinal(&context, XPLHASH_LOWERCASE, token, XPLHASH_MD5_LENGTH); + XplRandomData(random, sizeof(random)); + for (size_t i = 0; i < sizeof(random); i++) { + snprintf(token + (i * 2), 3, "%02x", random[i]); + } uint64_t expiration = time(NULL) + timeout; @@ -96,11 +94,11 @@ StoreCommandCOOKIENEW(StoreClient *client, uint64_t timeout) MsgSQLClose(db); - return ConnWriteF(client->conn, "1000 %.32s\r\n", token); + return ConnWriteF(client->conn, "1000 %s\r\n", token); abort: MsgSQLFinalize(&stmt); - MsgSQLAbortTransaction(db); + if (db) MsgSQLAbortTransaction(db); if (db) MsgSQLClose(db); return ConnWriteStr(client->conn, MSG5004INTERNALERR); } @@ -141,7 +139,7 @@ StoreCommandCOOKIEDELETE(StoreClient *client, const char *token) abort: MsgSQLFinalize(&stmt); - MsgSQLAbortTransaction(db); + if (db) MsgSQLAbortTransaction(db); if (db) MsgSQLClose(db); return ConnWriteStr(client->conn, MSG5004INTERNALERR); } @@ -167,7 +165,8 @@ StoreCommandAUTHCOOKIE(StoreClient *client, char *user, char *token, int nouser) int result; MsgSQLHandle *db = OpenCookieDB(client); - + if (db == NULL) return ConnWriteStr(client->conn, MSG5004INTERNALERR); + if (MsgSQLBeginTransaction(db)) return -2; memset(&stmt, 0, sizeof(MsgSQLStatement)); @@ -198,7 +197,7 @@ StoreCommandAUTHCOOKIE(StoreClient *client, char *user, char *token, int nouser) abort: MsgSQLFinalize(&stmt); - MsgSQLAbortTransaction(db); + if (db) MsgSQLAbortTransaction(db); if (db) MsgSQLClose(db); return ConnWriteStr(client->conn, MSG5004INTERNALERR); } @@ -210,7 +209,8 @@ StoreCommandCOOKIELIST(StoreClient *client) MsgSQLStatement *find = NULL; MsgSQLHandle *db = OpenCookieDB(client); - + if (db == NULL) return ConnWriteStr(client->conn, MSG5004INTERNALERR); + if (MsgSQLBeginTransaction(db)) return -2; memset(&stmt, 0, sizeof(MsgSQLStatement)); @@ -239,8 +239,7 @@ StoreCommandCOOKIELIST(StoreClient *client) abort: MsgSQLFinalize(&stmt); - MsgSQLAbortTransaction(db); + if (db) MsgSQLAbortTransaction(db); if (db) MsgSQLClose(db); return ConnWriteStr(client->conn, MSG5004INTERNALERR); } - diff --git a/src/agents/store/db.c b/src/agents/store/db.c index 5dbd851..349672f 100644 --- a/src/agents/store/db.c +++ b/src/agents/store/db.c @@ -91,7 +91,12 @@ StoreDBOpen(StoreClient *client, const char *user) // this bit is slow, but I can't think how we could // do this outside the dbpool.lock without being racy :( - snprintf(path, XPL_MAX_PATH, "%s/%s/store.db", StoreAgent.store.rootDir, user); + if (snprintf(path, sizeof(path), "%s/%s/store.db", + StoreAgent.store.rootDir, user) >= (int)sizeof(path)) { + BongoHashtableRemove(StoreAgent.dbpool.entries, (void *)user); + DBPoolEntryDelete(entry); + goto db_fail; + } entry->handle = MsgSQLOpen(path, &client->memstack, 3000); if (entry->handle == NULL) { BongoHashtableRemove(StoreAgent.dbpool.entries, (void *)user); diff --git a/src/agents/store/guid.c b/src/agents/store/guid.c index 2fc1125..e2e5ad4 100644 --- a/src/agents/store/guid.c +++ b/src/agents/store/guid.c @@ -36,7 +36,7 @@ void GuidReset(void) { #ifdef HAVE_KSTAT_H kstat_t *ksp; kstat_ctl_t *kc; -#elifdef HAVE_SYS_SYSINFO_H +#elif defined(HAVE_SYS_SYSINFO_H) struct sysinfo si; #endif struct timeval tv; diff --git a/src/agents/store/mail.c b/src/agents/store/mail.c index d691199..15b6f50 100644 --- a/src/agents/store/mail.c +++ b/src/agents/store/mail.c @@ -31,8 +31,8 @@ SetDocProp(StoreClient *client, StoreObject *doc, char *pname, char *value) } struct wanted_header { - const char const *header; - const char const *propname; + const char *header; + const char *propname; }; // properties we want to look for and set automatically @@ -45,6 +45,11 @@ static struct wanted_header header_list[] = { { "References", "bongo.references" }, { "X-Auth-OK", "bongo.xauthok" }, { "List-Id", "bongo.listid" }, + { "List-Post", "bongo.listpost" }, + { "List-Unsubscribe", "bongo.listunsubscribe" }, + { "List-Unsubscribe-Post", "bongo.listunsubscribepost" }, + { "List-Archive", "bongo.listarchive" }, + { "Auto-Submitted", "bongo.autosubmitted" }, { "Subject", "nmap.mail.subject" }, { NULL, NULL } }; @@ -238,7 +243,10 @@ StoreProcessIncomingMail(StoreClient *client, // save setting collection_guid til last, so that the new convo // 'appears' atomically conversation.collection_guid = STORE_CONVERSATIONS_GUID; - StoreObjectFixUpFilename(&convo_collection, &conversation); + if (StoreObjectFixUpFilename(&convo_collection, &conversation) != 0) { + result = MSG5005DBLIBERR; + goto finish; + } if (StoreObjectSave(client, &conversation) != 0) { result = MSG5005DBLIBERR; goto finish; @@ -261,4 +269,3 @@ finish: return result; } - diff --git a/src/agents/store/maildir.c b/src/agents/store/maildir.c index 68ac20e..5ba7021 100644 --- a/src/agents/store/maildir.c +++ b/src/agents/store/maildir.c @@ -112,7 +112,10 @@ MaildirRemove(const char *store_path, uint64_t collection) DIR *dir; struct dirent *content; - snprintf(rootpath, XPL_MAX_PATH, "%s" GUID_FMT "%s", store_path, collection, dirs[i]); + if (snprintf(rootpath, sizeof(rootpath), "%s" GUID_FMT "%s", + store_path, collection, dirs[i]) >= (int)sizeof(rootpath)) { + return ENAMETOOLONG; + } dir = opendir(rootpath); if (dir) { char filename[XPL_MAX_PATH+1]; @@ -122,7 +125,11 @@ MaildirRemove(const char *store_path, uint64_t collection) if ((c[0] == '.') && ((c[1] == '.') || (c[1] == '\0'))){ continue; } - snprintf(filename, XPL_MAX_PATH, "%s/%s", rootpath, content->d_name); + if (snprintf(filename, sizeof(filename), "%s/%s", + rootpath, content->d_name) >= (int)sizeof(filename)) { + closedir(dir); + return ENAMETOOLONG; + } if (unlink(filename) == -1) { // can't remove one of the contents closedir(dir); @@ -134,7 +141,10 @@ MaildirRemove(const char *store_path, uint64_t collection) if (rmdir(rootpath) != 0) return errno; } - snprintf(rootpath, XPL_MAX_PATH, "%s" GUID_FMT, store_path, collection); + if (snprintf(rootpath, sizeof(rootpath), "%s" GUID_FMT, + store_path, collection) >= (int)sizeof(rootpath)) { + return ENAMETOOLONG; + } if (rmdir(rootpath) != 0) return errno; return 0; diff --git a/src/agents/store/mime.c b/src/agents/store/mime.c index 08e4166..a2476f7 100644 --- a/src/agents/store/mime.c +++ b/src/agents/store/mime.c @@ -388,12 +388,12 @@ MimeParse(FILE *fh, size_t messageSize, char *line, unsigned long lineSize) do { /* Check to see if we've hit the end of the header. 'CR LF' is the only valid indicator */ /* but this will handle common broken alternatives such as 'CR CR LF' and 'LF' */ - unsigned long i = 0; + unsigned long line_index = 0; - while (line[i] == '\r') { - i++; + while (line[line_index] == '\r') { + line_index++; } - if (line[i] == '\n') { + if (line[line_index] == '\n') { lastHeaderLen = sPos - lastHeaderStart; if (!skip) { inHeader=FALSE; @@ -536,7 +536,7 @@ MimeParse(FILE *fh, size_t messageSize, char *line, unsigned long lineSize) doingDigest = TRUE; } ParseTypeParameters(end + 1, &part[workPartNo]); - if (part[workPartNo].separator) { + if (part[workPartNo].separator[0] != '\0') { currentSeparator = part[workPartNo].separator; } } diff --git a/src/agents/store/object-model.c b/src/agents/store/object-model.c index e9953f0..426b2a0 100644 --- a/src/agents/store/object-model.c +++ b/src/agents/store/object-model.c @@ -188,18 +188,24 @@ StorePropFixupNames(StorePropInfo *props, int propcount) } // fix up the filename in object based on its container -void +int StoreObjectFixUpFilename(StoreObject *container, StoreObject *object) { + int length; + // filename must be unique in the store! if (object->filename[0] == '\0') { // create a temp name if we weren't given one - snprintf(object->filename, MAX_FILE_NAME, "%s/bongo-" GUID_FMT, container->filename, object->guid); + length = snprintf(object->filename, sizeof(object->filename), + "%s/bongo-" GUID_FMT, container->filename, object->guid); + if (length < 0 || length >= (int)sizeof(object->filename)) { + object->filename[0] = '\0'; + return -1; + } } else { // need to prepand the filename of the parent collection // FIXME - should collections end with '/' ? char new_name[MAX_FILE_NAME]; - char *format = "%s/%s"; char *filepart; filepart = strrchr(object->filename, '/'); @@ -209,12 +215,20 @@ StoreObjectFixUpFilename(StoreObject *container, StoreObject *object) filepart++; // don't include separating / when prepending '/' - if (container->filename[1] == '\0') - format = "%s%s"; - - snprintf(new_name, MAX_FILE_NAME, format, container->filename, filepart); - strncpy(object->filename, new_name, MAX_FILE_NAME); + if (container->filename[1] == '\0') { + length = snprintf(new_name, sizeof(new_name), "%s%s", + container->filename, filepart); + } else { + length = snprintf(new_name, sizeof(new_name), "%s/%s", + container->filename, filepart); + } + if (length < 0 || length >= (int)sizeof(new_name)) { + return -1; + } + memcpy(object->filename, new_name, (size_t)length + 1); } + + return 0; } /** @@ -275,9 +289,11 @@ StoreObjectCreate(StoreClient *client, StoreObject *object) if (MsgSQLCommitTransaction(client->storedb)) goto abort; - if (object->filename[0] != '/') + if (object->filename[0] != '/') { // assume we need to fix the filename if it's not rooted. - StoreObjectFixUpFilename(&root_collection, object); + if (StoreObjectFixUpFilename(&root_collection, object) != 0) + goto abort; + } // create any files etc. needed if (STORE_IS_FOLDER(object->type)) { @@ -673,13 +689,14 @@ StoreOutputProperty(StoreClient *client, StorePropertyType type, const char *nam { if (value != NULL) { const char *out = value; + char outval[65]; int outlen; if ((type == STORE_PROP_CREATED) || (type == STORE_PROP_LASTMODIFIED)) { // need to turn an int into a date. struct tm tm; time_t tt; - char *endp, outval[65]; + char *endp; uint64_t intval; intval = strtol(value, &endp, 10); @@ -710,7 +727,7 @@ StoreObjectIterQueryBuilderPropResult(StoreClient *client, // start value below "col_id + 9" depends on how many columns // we pull back in our initial query; careful :) for (col_id = 0, result_id = 0; col_id < properties; col_id++) { - const StorePropInfo const *prop = g_ptr_array_index(builder->properties, col_id); + const StorePropInfo *prop = g_ptr_array_index(builder->properties, col_id); // ignore properties we don't want output; if (prop->output == FALSE) continue; @@ -1217,7 +1234,8 @@ StoreObjectRemoveCollection(StoreClient *client, StoreObject *collection) ret = MsgSQLPrepare(client->storedb, query, &stmt); if (ret == NULL) goto abort; - snprintf(path, MAX_FILE_NAME, "%s/", collection->filename); + if (snprintf(path, sizeof(path), "%s/", collection->filename) >= (int)sizeof(path)) + goto abort; MsgSQLBindString(&stmt, 1, path, FALSE); MsgSQLBindInt(&stmt, 2, strlen(path)); MsgSQLExecute(client->storedb, &stmt); @@ -1299,8 +1317,10 @@ StoreObjectRenameSubobjects(StoreClient *client, StoreObject *container, const c return 0; // nothing to do memset(&stmt, 0, sizeof(MsgSQLStatement)); - snprintf(path, MAX_FILE_NAME, "%s/", container->filename); - snprintf(newpath, MAX_FILE_NAME, "%s/", new_root); + if (snprintf(path, sizeof(path), "%s/", container->filename) >= (int)sizeof(path)) + return -1; + if (snprintf(newpath, sizeof(newpath), "%s/", new_root) >= (int)sizeof(newpath)) + return -1; MsgSQLBeginTransaction(client->storedb); @@ -1336,11 +1356,13 @@ int StoreObjectSetFilename(StoreObject *object, const StoreObject *collection, const char *filename) { if (filename == NULL) { - snprintf(object->filename, sizeof(object->filename), "%s/bongo-" GUID_FMT, - collection->filename, object->guid); + if (snprintf(object->filename, sizeof(object->filename), "%s/bongo-" GUID_FMT, + collection->filename, object->guid) >= (int)sizeof(object->filename)) + return -1; } else { - snprintf(object->filename, sizeof(object->filename), "%s/%s", - collection->filename, filename); + if (snprintf(object->filename, sizeof(object->filename), "%s/%s", + collection->filename, filename) >= (int)sizeof(object->filename)) + return -1; } return 0; } @@ -1784,16 +1806,16 @@ ACLCheckOnGUID(StoreClient *client, uint64_t guid, int prop) while ((status = MsgSQLResults(client->storedb, &stmt)) > 0) { int priv; int type; - int deny; + int row_denies; char *who; BOOL *grant; priv = MsgSQLResultInt(&stmt, 0); type = MsgSQLResultInt(&stmt, 1); - deny = MsgSQLResultInt(&stmt, 2); + row_denies = MsgSQLResultInt(&stmt, 2); MsgSQLResultTextPtr(&stmt, 3, &who); - if (deny == 0) { + if (row_denies == 0) { grant = &allow; } else { grant = &deny; diff --git a/src/agents/store/object-model.h b/src/agents/store/object-model.h index 51a01f4..2919575 100644 --- a/src/agents/store/object-model.h +++ b/src/agents/store/object-model.h @@ -80,7 +80,7 @@ int StoreObjectListACL(StoreClient *client, StoreObject *object); int StoreObjectSetProperty(StoreClient *client, StoreObject *object, StorePropInfo *prop); int StoreObjectSetExternalProperty(StoreClient *client, StoreObject *object, StorePropInfo *prop); int StoreObjectSetFilename(StoreObject *object, const StoreObject *collection, const char *filename); -void StoreObjectFixUpFilename(StoreObject *container, StoreObject *object); +int StoreObjectFixUpFilename(StoreObject *container, StoreObject *object); void StoreObjectUpdateModifiedTime(StoreObject *object); // iterators on store contents. diff --git a/src/agents/store/query-builder.c b/src/agents/store/query-builder.c index de8925c..3b677e5 100644 --- a/src/agents/store/query-builder.c +++ b/src/agents/store/query-builder.c @@ -30,9 +30,9 @@ QueryBuilderStart(QueryBuilder *builder) builder->order_direction = ORDER_NONE; builder->output_mode = MODE_COLLECTIONS; - builder->properties = g_ptr_array_new(); - builder->links = g_ptr_array_new(); - builder->parameters = g_ptr_array_new(); + builder->properties = g_ptr_array_new_with_free_func(g_free); + builder->links = g_ptr_array_new_with_free_func(g_free); + builder->parameters = g_ptr_array_new_with_free_func(g_free); return 0; } @@ -48,13 +48,10 @@ QueryBuilderFinish(QueryBuilder *builder) QueryParserFinish(&builder->external_parser); /* free all the properties, links and parameters */ - g_ptr_array_foreach (builder->properties, (GFunc)g_free, NULL); g_ptr_array_free(builder->properties, TRUE); - g_ptr_array_foreach (builder->links, (GFunc)g_free, NULL); g_ptr_array_free(builder->links, TRUE); - g_ptr_array_foreach (builder->parameters, (GFunc)g_free, NULL); g_ptr_array_free(builder->parameters, TRUE); } @@ -503,7 +500,8 @@ QueryExpressionToSQL(QueryBuilder *builder, struct expression *exp, BongoStringB val = malloc(len + 4); val[0] = '\''; val[1] = '%'; char *expression = (char*)exp->exp2; - for(int i=1; i < len; i++) val[i+1] = expression[i]; + for (int value_index = 1; value_index < len; value_index++) + val[value_index + 1] = expression[value_index]; val[len] = '%'; val[len+1] = '\''; val[len+2] = '\0'; diff --git a/src/agents/store/search.c b/src/agents/store/search.c index ff4107f..a307b08 100644 --- a/src/agents/store/search.c +++ b/src/agents/store/search.c @@ -73,10 +73,12 @@ SearchDocument(StoreClient *client, StoreObject *document, if (query->type == STORE_SEARCH_HEADERS) { break; } - /* nothing found in headers; fall through to STORE_SEARCH_BODY */ + goto search_body; } case STORE_SEARCH_BODY: { +search_body: + ; MimeReport *report = NULL; unsigned i; diff --git a/src/agents/store/store.c b/src/agents/store/store.c index ce919d9..f872577 100644 --- a/src/agents/store/store.c +++ b/src/agents/store/store.c @@ -183,8 +183,16 @@ SelectStore(StoreClient *client, char *user) // close current selected store if necessary UnselectStore(client); - snprintf(path, XPL_MAX_PATH, "%s/%s/", StoreAgent.store.rootDir, user); - strcpy(client->store, path); + if (snprintf(path, sizeof(path), "%s/%s/", + StoreAgent.store.rootDir, user) >= (int)sizeof(path)) { + Log(LOG_ERROR, "Store path is too long for user %s", user); + return -1; + } + if (strlen(path) >= sizeof(client->store)) { + Log(LOG_ERROR, "Store path does not fit the client buffer for user %s", user); + return -1; + } + memcpy(client->store, path, strlen(path) + 1); client->storeRoot = storeRoot; client->storeHash = BongoStringHash(user); diff --git a/src/agents/store/stored.c b/src/agents/store/stored.c index d288dda..5feab02 100644 --- a/src/agents/store/stored.c +++ b/src/agents/store/stored.c @@ -84,10 +84,15 @@ ProcessEntry(void *clientp, ccode = -1; } else if (!IS_MANAGER(client)) { /* issue nmap challenge */ - sprintf(client->authToken, "%x%s%x", - (unsigned int) XplGetThreadID(), - StoreAgent.server.host, - (unsigned int) time(NULL)); + unsigned char challenge[(sizeof(client->authToken) - 1) / 2]; + size_t challenge_index; + + XplRandomData(challenge, sizeof(challenge)); + for (challenge_index = 0; challenge_index < sizeof(challenge); challenge_index++) { + snprintf(client->authToken + challenge_index * 2, + sizeof(client->authToken) - challenge_index * 2, + "%02x", challenge[challenge_index]); + } ccode = ConnWriteF(client->conn, MSG4242AUTHREQUIRED, client->authToken); } else { ccode = ConnWriteF(client->conn, "1000 %s %s\r\n", diff --git a/src/agents/store/stored.h b/src/agents/store/stored.h index 8b478b0..01fe14f 100644 --- a/src/agents/store/stored.h +++ b/src/agents/store/stored.h @@ -185,7 +185,8 @@ struct _StoreClient { StoreToken *tokens; } principal; - char authToken[64]; + /* 256-bit challenge encoded as 64 hex digits plus the NUL terminator. */ + char authToken[65]; struct { StoreObject collection; diff --git a/src/apps/admin/bongo/admintool/DemoCommands.py b/src/apps/admin/bongo/admintool/DemoCommands.py index e80bac2..a9eac0c 100644 --- a/src/apps/admin/bongo/admintool/DemoCommands.py +++ b/src/apps/admin/bongo/admintool/DemoCommands.py @@ -1,5 +1,5 @@ -import bongo.external.simplejson as simplejson -import bongo.external.vobject as vobject +import json as simplejson +import vobject import logging import os import re @@ -9,10 +9,10 @@ import hashlib import urllib.request, urllib.parse, urllib.error import email -from email.MIMEText import MIMEText -from email.MIMEMessage import MIMEMessage -from email.MIMEMultipart import MIMEMultipart -from email.Message import Message +from email.mime.text import MIMEText +from email.mime.message import MIMEMessage +from email.mime.multipart import MIMEMultipart +from email.message import Message import bongo.table as table diff --git a/src/apps/config/config.c b/src/apps/config/config.c index ef3da4a..50e1343 100644 --- a/src/apps/config/config.c +++ b/src/apps/config/config.c @@ -73,8 +73,6 @@ GetInstallParameters(void){ config.domains = BongoJsonNodeNewArray(g_array_new(FALSE, FALSE, sizeof(char *))); while (1) { char *tmp = NULL; - BongoJsonNode *domain; - GetInteractiveData(_("Mail Domains (one per line, blank line to finish):"), &tmp, ""); if (tmp == NULL) { return; @@ -133,8 +131,8 @@ SetStoreConfigurationModifications(StoreClient *client) { /* add the default configuration document for each domain */ for(x=0;xlen;x++) { - BongoJsonNode *current = BongoJsonArrayGet(domains, x); - PutOrReplaceConfig(client, "/config/aliases", BongoJsonNodeAsString(current), default_config, default_config_len); + BongoJsonNode *domain_node = BongoJsonArrayGet(domains, x); + PutOrReplaceConfig(client, "/config/aliases", BongoJsonNodeAsString(domain_node), default_config, default_config_len); } /* put back the old domains array so that the free can sanely free it */ @@ -181,7 +179,7 @@ void InitializeDataArea(void) { MsgApiDirectory dir; - const unsigned char *unpriv_user; + const char *unpriv_user; uid_t uid = 0; gid_t gid = 0; @@ -315,25 +313,45 @@ GetInteractiveData(char *description, char **data, char *def) { #define CERTSIZE 10240 +static BOOL +WriteCryptoFile(const char *path, const unsigned char *data, size_t length, mode_t mode) +{ + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, mode); + FILE *stream; + BOOL success; + + if (fd < 0) + return FALSE; + if (fchmod(fd, mode) != 0) { + close(fd); + return FALSE; + } + stream = fdopen(fd, "wb"); + if (stream == NULL) { + close(fd); + return FALSE; + } + success = fwrite(data, 1, length, stream) == length && fclose(stream) == 0; + return success; +} + BOOL GenerateCryptoData() { - static gnutls_dh_params_t dh_params; - static gnutls_rsa_params_t rsa_params; - gnutls_x509_privkey key; - gnutls_x509_crt certificate; - unsigned char dhdata[CERTSIZE], rsadata[CERTSIZE], private_key[CERTSIZE], public_cert[CERTSIZE]; + gnutls_x509_privkey_t key = NULL; + gnutls_x509_crt_t certificate = NULL; + unsigned char private_key[CERTSIZE], public_cert[CERTSIZE]; + unsigned char serial[16]; int ret; - size_t dsize = sizeof(unsigned char) * (CERTSIZE); - int cert_version = 1; - mode_t private = S_IRUSR | S_IWUSR; - FILE *params; + size_t dsize; + BOOL success = FALSE; static const char *org = "Bongo Project"; - static const char *unit = "Fake Certs Dept."; - static const char *state = "None"; - static const char *country = "US"; - static const char *blank = ""; + const char *hostname = config.dns ? config.dns : "localhost"; - gnutls_global_init(); + ret = gnutls_global_init(); + if (ret < 0) { + XplConsolePrintf(_("ERROR: Couldn't initialize TLS. %s\n"), gnutls_strerror(ret)); + return FALSE; + } // hack: we need to ensure the files below can be saved... if (0 != mkdir(XPL_DEFAULT_DBF_DIR, 0755)) { @@ -345,155 +363,81 @@ GenerateCryptoData() { // save a random seed for faster Xpl startup in future. XplSaveRandomSeed(); - // various magic params - gnutls_dh_params_init(&dh_params); - gnutls_dh_params_generate2(dh_params, 1024); - dsize = CERTSIZE; - gnutls_dh_params_export_pkcs3 (dh_params, GNUTLS_X509_FMT_PEM, dhdata, &dsize); - params = fopen(XPL_DEFAULT_DHPARAMS_PATH, "w"); - chmod(XPL_DEFAULT_DHPARAMS_PATH, private); - if (params) { - fprintf(params, "%s\n", dhdata); - fclose(params); - } else { - gnutls_dh_params_deinit(dh_params); - gnutls_global_deinit(); - XplConsolePrintf(_("ERROR: Couldn't write DH params to %s\n"), XPL_DEFAULT_DHPARAMS_PATH); - return FALSE; - } - - gnutls_rsa_params_init(&rsa_params); - gnutls_rsa_params_generate2(rsa_params, 512); - dsize = CERTSIZE; - gnutls_rsa_params_export_pkcs1 (rsa_params, GNUTLS_X509_FMT_PEM, rsadata, &dsize); - params = fopen(XPL_DEFAULT_RSAPARAMS_PATH, "w"); - if (params) { - chmod(XPL_DEFAULT_RSAPARAMS_PATH, private); - fprintf(params, "%s\n", dhdata); - fclose(params); - } else { - gnutls_dh_params_deinit(dh_params); - gnutls_rsa_params_deinit(rsa_params); - gnutls_global_deinit(); - XplConsolePrintf(_("ERROR: Couldn't write RSA params to %s\n"), XPL_DEFAULT_DHPARAMS_PATH); - return FALSE; - } - // create private key ret = gnutls_x509_privkey_init(&key); + if (ret < 0) + goto tls_error; + ret = gnutls_x509_privkey_generate(key, GNUTLS_PK_RSA, 3072, 0); if (ret < 0) { - gnutls_dh_params_deinit(dh_params); - gnutls_rsa_params_deinit(rsa_params); - gnutls_global_deinit(); - XplConsolePrintf(_("ERROR: %s\n"), gnutls_strerror (ret)); - return FALSE; - } - ret = gnutls_x509_privkey_generate(key, GNUTLS_PK_RSA, 1024, 0); - if (ret < 0) { - gnutls_dh_params_deinit(dh_params); - gnutls_rsa_params_deinit(rsa_params); - gnutls_global_deinit(); XplConsolePrintf(_("ERROR: Couldn't create private key. %s\n"), gnutls_strerror(ret)); - return FALSE; + goto cleanup; } dsize = CERTSIZE; - ret = gnutls_x509_privkey_export(key, GNUTLS_X509_FMT_PEM, &private_key, &dsize); + ret = gnutls_x509_privkey_export(key, GNUTLS_X509_FMT_PEM, private_key, &dsize); if (ret < 0) { - gnutls_dh_params_deinit(dh_params); - gnutls_rsa_params_deinit(rsa_params); - gnutls_global_deinit(); XplConsolePrintf(_("ERROR: Couldn't export private key. %s\n"), gnutls_strerror(ret)); - XplConsolePrintf(_(" Size: %zd, Ret: %d\n"), dsize, ret); - return FALSE; - } else { - params = fopen(XPL_DEFAULT_KEY_PATH, "w"); - if (params) { - chmod(XPL_DEFAULT_KEY_PATH, private); - fprintf(params, "%s\n", private_key); - fclose(params); - } else { - gnutls_dh_params_deinit(dh_params); - gnutls_rsa_params_deinit(rsa_params); - gnutls_global_deinit(); - XplConsolePrintf(_("ERROR: Couldn't write private key to %s\n"), XPL_DEFAULT_KEY_PATH); - return FALSE; - } + goto cleanup; + } + if (!WriteCryptoFile(XPL_DEFAULT_KEY_PATH, private_key, dsize, S_IRUSR | S_IWUSR)) { + XplConsolePrintf(_("ERROR: Couldn't write private key to %s\n"), XPL_DEFAULT_KEY_PATH); + goto cleanup; } // create certificate ret = gnutls_x509_crt_init(&certificate); - if (ret < 0) { - gnutls_dh_params_deinit(dh_params); - gnutls_rsa_params_deinit(rsa_params); - gnutls_global_deinit(); - XplConsolePrintf(_("ERROR: %s\n"), gnutls_strerror(ret)); - return FALSE; - } + if (ret < 0) + goto tls_error; gnutls_x509_crt_set_activation_time(certificate, time(NULL)); - gnutls_x509_crt_set_expiration_time(certificate, time(NULL) + (700 * 24 * 60 * 60)); + gnutls_x509_crt_set_expiration_time(certificate, time(NULL) + (10 * 365 * 24 * 60 * 60)); gnutls_x509_crt_set_key(certificate, key); - gnutls_x509_crt_set_version(certificate, 1); - gnutls_x509_crt_set_serial(certificate, &cert_version, sizeof(int)); + gnutls_x509_crt_set_version(certificate, 3); + XplRandomData(serial, sizeof(serial)); + serial[0] &= 0x7f; + gnutls_x509_crt_set_serial(certificate, serial, sizeof(serial)); gnutls_x509_crt_set_dn_by_oid(certificate, GNUTLS_OID_X520_ORGANIZATION_NAME, 0, org, strlen(org)); gnutls_x509_crt_set_dn_by_oid(certificate, - GNUTLS_OID_X520_ORGANIZATIONAL_UNIT_NAME, 0, unit, strlen(unit)); - gnutls_x509_crt_set_dn_by_oid(certificate, - GNUTLS_OID_X520_STATE_OR_PROVINCE_NAME, 0, state, strlen(state)); - gnutls_x509_crt_set_dn_by_oid(certificate, - GNUTLS_OID_X520_COUNTRY_NAME, 0, country, strlen(country)); - gnutls_x509_crt_set_dn_by_oid(certificate, - GNUTLS_OID_X520_LOCALITY_NAME, 0, blank, strlen(blank)); - gnutls_x509_crt_set_dn_by_oid(certificate, - GNUTLS_OID_X520_COMMON_NAME, 0, config.dns, strlen(config.dns)); - gnutls_x509_crt_set_dn_by_oid(certificate, - GNUTLS_OID_LDAP_UID, 0, blank, strlen(blank)); + GNUTLS_OID_X520_COMMON_NAME, 0, hostname, strlen(hostname)); gnutls_x509_crt_set_subject_alternative_name (certificate, - GNUTLS_SAN_DNSNAME, config.dns); + GNUTLS_SAN_DNSNAME, hostname); gnutls_x509_crt_set_key_usage(certificate, GNUTLS_KEY_DIGITAL_SIGNATURE | GNUTLS_KEY_DATA_ENCIPHERMENT); ret = GNUTLS_SELF_SIGN(certificate, key); if (ret < 0) { - gnutls_dh_params_deinit(dh_params); - gnutls_rsa_params_deinit(rsa_params); - gnutls_global_deinit(); XplConsolePrintf(_("ERROR: Couldn't self-sign certificate. %s\n"), gnutls_strerror(ret)); - return FALSE; + goto cleanup; } dsize = CERTSIZE; - ret = gnutls_x509_crt_export(certificate, GNUTLS_X509_FMT_PEM, &public_cert, &dsize); + ret = gnutls_x509_crt_export(certificate, GNUTLS_X509_FMT_PEM, public_cert, &dsize); if (ret < 0) { - gnutls_dh_params_deinit(dh_params); - gnutls_rsa_params_deinit(rsa_params); - gnutls_global_deinit(); XplConsolePrintf(_("ERROR: Couldn't create certificate. %s\n"), gnutls_strerror(ret)); - return FALSE; + goto cleanup; } - params = fopen(XPL_DEFAULT_CERT_PATH, "w"); - if (params) { - fprintf(params, "%s\n", public_cert); - fclose(params); - } else { - gnutls_dh_params_deinit(dh_params); - gnutls_rsa_params_deinit(rsa_params); - gnutls_global_deinit(); + if (!WriteCryptoFile(XPL_DEFAULT_CERT_PATH, public_cert, dsize, + S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) { XplConsolePrintf(_("ERROR: Couldn't write certificate to %s\n"), XPL_DEFAULT_CERT_PATH); - return FALSE; + goto cleanup; } - - gnutls_x509_crt_deinit(certificate); - gnutls_x509_privkey_deinit(key); - gnutls_dh_params_deinit(dh_params); - gnutls_rsa_params_deinit(rsa_params); + + success = TRUE; + goto cleanup; + +tls_error: + XplConsolePrintf(_("ERROR: %s\n"), gnutls_strerror(ret)); +cleanup: + if (certificate) + gnutls_x509_crt_deinit(certificate); + if (key) + gnutls_x509_privkey_deinit(key); gnutls_global_deinit(); - - XplConsolePrintf(_("Crypto data generated.\n")); - - return TRUE; + + if (success) + XplConsolePrintf(_("Crypto data generated.\n")); + return success; } void @@ -701,24 +645,24 @@ main(int argc, char *argv[]) { if (next_arg >= argc) { XplConsolePrintf(_("USAGE : user [add|password|list] []\n")); } else { - char *command = argv[next_arg++]; + char *user_command = argv[next_arg++]; - if (!strncmp(command, "list", 4)) { + if (!strncmp(user_command, "list", 4)) { UserList(); } else { char *username; if (next_arg >= argc) { - XplConsolePrintf(_("USAGE : user %s = + (int)sizeof(currentpath) || + snprintf(fullpath, sizeof(fullpath), "CREATE %s\r\n", currentpath) >= + (int)sizeof(fullpath)) { + XplConsolePrintf(_("ERROR: Configuration path is too long\n")); + fclose(config); + return FALSE; + } result = NMAPSimpleCommand(client, fullpath); if ((result==1000) || (result==4226)) { @@ -80,6 +86,12 @@ ImportSystemBackupFile(StoreClient *client, const char *path) } delim = strrchr(filename, '/'); + if (delim == NULL) { + MemFree(file); + XplConsolePrintf(_("ERROR: Invalid configuration archive path: %s\n"), filename); + fclose(config); + return FALSE; + } *delim = 0; snprintf(fullpath, 108, "/%s", filename); if (!PutOrReplaceConfig(client, fullpath, diff --git a/src/apps/manager/manager.c b/src/apps/manager/manager.c index 2a6b14b..7f4fb67 100644 --- a/src/apps/manager/manager.c +++ b/src/apps/manager/manager.c @@ -561,8 +561,11 @@ ParentSignalHandler(int signo) switch (signo) { case SIGCHLD: ChildError = TRUE; + Exiting = TRUE; + break; case SIGUSR1: Exiting = TRUE; + break; } } diff --git a/src/apps/storetool/CMakeLists.txt b/src/apps/storetool/CMakeLists.txt index 65ec562..08e20c5 100644 --- a/src/apps/storetool/CMakeLists.txt +++ b/src/apps/storetool/CMakeLists.txt @@ -1,6 +1,8 @@ install(DIRECTORY ./bongo DESTINATION "${PYTHON_SITEPACKAGES_PATH}/" - FILES_MATCHING PATTERN "*.py" + FILES_MATCHING + PATTERN "__pycache__" EXCLUDE + PATTERN "*.py" PATTERN ".svn" EXCLUDE ) diff --git a/src/apps/storetool/bongo/storetool/CalendarCommands.py b/src/apps/storetool/bongo/storetool/CalendarCommands.py index 871c240..7c8bee5 100644 --- a/src/apps/storetool/bongo/storetool/CalendarCommands.py +++ b/src/apps/storetool/bongo/storetool/CalendarCommands.py @@ -1,5 +1,5 @@ -import bongo.external.simplejson as simplejson -import bongo.external.vobject as vobject +import json as simplejson +import vobject import logging import os import re @@ -8,10 +8,10 @@ import random import hashlib import email -from email.MIMEText import MIMEText -from email.MIMEMessage import MIMEMessage -from email.MIMEMultipart import MIMEMultipart -from email.Message import Message +from email.mime.text import MIMEText +from email.mime.message import MIMEMessage +from email.mime.multipart import MIMEMultipart +from email.message import Message import bongo.table as table diff --git a/src/apps/storetool/bongo/storetool/ContactCommands.py b/src/apps/storetool/bongo/storetool/ContactCommands.py index ef838d3..7584d82 100644 --- a/src/apps/storetool/bongo/storetool/ContactCommands.py +++ b/src/apps/storetool/bongo/storetool/ContactCommands.py @@ -1,5 +1,5 @@ -import bongo.external.simplejson as simplejson -import bongo.external.vobject as vobject +import json as simplejson +import vobject import bongo.table as table import logging diff --git a/src/apps/storetool/bongo/storetool/ExportMailbox.py b/src/apps/storetool/bongo/storetool/ExportMailbox.py index e5ece62..54c8409 100644 --- a/src/apps/storetool/bongo/storetool/ExportMailbox.py +++ b/src/apps/storetool/bongo/storetool/ExportMailbox.py @@ -4,29 +4,27 @@ import socket import errno import io import mailbox -import email, email.Generator +import email +from email.generator import Generator +from email.utils import parsedate_tz, mktime_tz import sys from time import time, mktime, strptime, ctime, sleep -from rfc822 import Message, parsedate_tz, mktime_tz - -class MboxMailbox(mailbox.PortableUnixMailbox): +class MboxMailbox: def __init__(self,file): - mailbox.PortableUnixMailbox.__init__(self, file, email.message_from_file) self.boxname = file self.content = "" def add(self, msg): fp = io.StringIO() - g = email.Generator.Generator(fp, mangle_from_=False) + g = Generator(fp, mangle_from_=False) envelope = email.message_from_string(msg) g.flatten(envelope, unixfrom=True) self.content += "%s" % fp.getvalue() def write(self): - outfile=open(self.boxname, "wb") - outfile.write("%s\n" % self.content) - outfile.close() + with open(self.boxname, "w", encoding="utf-8", newline="") as outfile: + outfile.write("%s\n" % self.content) class MaildirMailbox(mailbox.Maildir): def __init__(self,file): @@ -35,14 +33,8 @@ class MaildirMailbox(mailbox.Maildir): else: self.boxname = file + "/" - while 1: - try: - os.makedirs(self.boxname + "cur") - os.makedirs(self.boxname + "new") - os.makedirs(self.boxname + "tmp") - except OSError as err: - if err.errno == errno.EEXIST: - break + for directory in ("cur", "new", "tmp"): + os.makedirs(os.path.join(self.boxname, directory), exist_ok=True) mailbox.Maildir.__init__(self, file, email.message_from_file) def tmp_open(self): @@ -83,12 +75,14 @@ class MaildirMailbox(mailbox.Maildir): def write_message(self, raw_msg, fd, tmp): try: + if isinstance(raw_msg, str): + raw_msg = raw_msg.encode("utf-8") os.write(fd, raw_msg) os.fsync(fd) os.close(fd) except OSError as err: os.unlink(tmp) - raise Error("unable to fsync() or close() temp file: %s" % err) + raise OSError("unable to fsync() or close() temp file: %s" % err) def finish_message(self, tmp, dir, info, dtime): @@ -109,7 +103,7 @@ class MaildirMailbox(mailbox.Maildir): try: fp = io.StringIO(raw_msg) - message = Message(fp) + message = email.message_from_file(fp) (dir, info) = self.status(message) dtime = self.get_delivery_time(message) self.write_message(raw_msg, fd, tmp) diff --git a/src/apps/storetool/bongo/storetool/MailCommands.py b/src/apps/storetool/bongo/storetool/MailCommands.py index c759815..d2074f0 100644 --- a/src/apps/storetool/bongo/storetool/MailCommands.py +++ b/src/apps/storetool/bongo/storetool/MailCommands.py @@ -145,19 +145,15 @@ class MailImapCommand(Command): collection = "/mail/" + folder store.Create(collection, existingOk=True) - try: - M=imaplib.IMAP4(imap_server, imap_port) - M.login(imap_username, imap_password) - M.select(imap_folder) - - try: - typ, data = M.search(None, 'ALL') - for num in data[0].split(): - store.WriteImap(collection, DocTypes.Mail, M, num) + M=imaplib.IMAP4(imap_server, imap_port) + M.login(imap_username, imap_password) + M.select(imap_folder) - finally: - M.close() - M.logout() + try: + typ, data = M.search(None, 'ALL') + for num in data[0].split(): + store.WriteImap(collection, DocTypes.Mail, M, num) finally: - return + M.close() + M.logout() diff --git a/src/libs/cal/bongo-cal-instance.c b/src/libs/cal/bongo-cal-instance.c index dc14ed3..565df88 100644 --- a/src/libs/cal/bongo-cal-instance.c +++ b/src/libs/cal/bongo-cal-instance.c @@ -394,7 +394,7 @@ ReadRecur(BongoJsonObject *obj, BongoCalRule *recur) recur->bymonth = IntArrayToList(arrayVal, 1, 12); for (l = recur->bymonth; l != NULL; l = l->next) { - l->data--; + l->data = BongoListIntToVoid(BongoListVoidToInt(l->data) - 1); } } diff --git a/src/libs/cal/bongo-cal-time.c b/src/libs/cal/bongo-cal-time.c index e6f0b03..aa37ab8 100644 --- a/src/libs/cal/bongo-cal-time.c +++ b/src/libs/cal/bongo-cal-time.c @@ -463,7 +463,7 @@ BongoCalTimeToStringConcise(BongoCalTime t, char *buffer, int buflen) { uint64_t tnow, twhen; BongoCalTime now; - char tmbuf[10]; + char tmbuf[32]; now = BongoCalTimeNow(NULL); tnow = BongoCalTimeAsUint64(now); @@ -510,7 +510,7 @@ BongoCalTimeAdjust(BongoCalTime t, int days, int hours, int minutes, int seconds { int second, minute, hour, day; int minutesOverflow = 0, hoursOverflow = 0, daysOverflow = 0, yearsOverflow = 0; - int daysInMonth; + int monthDays; ASSERTTZ(t); @@ -559,8 +559,8 @@ BongoCalTimeAdjust(BongoCalTime t, int days, int hours, int minutes, int seconds day = t.day + days + daysOverflow; if (day > 0) { while (1) { - daysInMonth = BongoCalDaysInMonth(t.month, t.year); - if (day <= daysInMonth) { + monthDays = BongoCalDaysInMonth(t.month, t.year); + if (day <= monthDays) { break; } t.month++; @@ -569,7 +569,7 @@ BongoCalTimeAdjust(BongoCalTime t, int days, int hours, int minutes, int seconds t.month = 0; } - day -= daysInMonth; + day -= monthDays; } } else { while (day <= 0) { @@ -769,4 +769,3 @@ BongoCalDurationParseIcal(const char *str) error: return emptyDuration; } - diff --git a/src/libs/connio/connio.c b/src/libs/connio/connio.c index f89d534..c672cfa 100644 --- a/src/libs/connio/connio.c +++ b/src/libs/connio/connio.c @@ -50,46 +50,60 @@ #ifdef HAVE_NETINET_IN_H #include #endif +#include #include #include "conniop.h" ConnIOGlobals ConnIO; -gnutls_dh_params_t dh_params; -gnutls_rsa_params_t rsa_params; - BOOL -__gnutls_new(Connection *conn, bongo_ssl_context *context, gnutls_connection_end_t con_end) { +__gnutls_new(Connection *conn, bongo_ssl_context *context, unsigned int flags) { int ccode; conn->ssl.context = NULL; conn->ssl.credentials = NULL; - ccode = gnutls_init(&conn->ssl.context, con_end); - if (ccode) { + ccode = gnutls_init(&conn->ssl.context, flags); + if (ccode < 0) { return FALSE; } - if (con_end == GNUTLS_SERVER) { - ccode = gnutls_set_default_export_priority(conn->ssl.context); + /* Secure by default. The compatibility profile is deliberately opt-in + * for legacy clients and still offers modern protocol versions first. */ + ccode = gnutls_priority_set_direct( + conn->ssl.context, + (context->options & SSL_ALLOW_LEGACY_PROTOCOLS) + ? "LEGACY:-VERS-ALL:+VERS-TLS1.3:+VERS-TLS1.2:+VERS-TLS1.1:+VERS-TLS1.0:+VERS-SSL3.0" + : "NORMAL:-VERS-ALL:+VERS-TLS1.3:+VERS-TLS1.2", + NULL); + if (ccode < 0) { + gnutls_deinit(conn->ssl.context); + conn->ssl.context = NULL; + return FALSE; + } - /* store in the credetials loaded earler */ + if (flags & GNUTLS_SERVER) { ccode = gnutls_credentials_set(conn->ssl.context, GNUTLS_CRD_CERTIFICATE, context->cert_cred); } else { - /* defaults are ok here */ - gnutls_set_default_priority (conn->ssl.context); + ccode = gnutls_certificate_allocate_credentials(&conn->ssl.credentials); + if (ccode >= 0) { + ccode = gnutls_certificate_set_x509_system_trust(conn->ssl.credentials); + } + if (ccode >= 0) { + ccode = gnutls_credentials_set(conn->ssl.context, GNUTLS_CRD_CERTIFICATE, + conn->ssl.credentials); + } + } - /* store the priority for x509 or openpgp out there - * i doubt that openpgp will be used but perhaps there is a server that supports it */ - gnutls_certificate_allocate_credentials (&conn->ssl.credentials); - gnutls_certificate_set_x509_trust_file (conn->ssl.credentials, XPL_DEFAULT_CERT_PATH, GNUTLS_X509_FMT_PEM); - - /* set the empty credentials in the session */ - gnutls_credentials_set (conn->ssl.context, GNUTLS_CRD_CERTIFICATE, conn->ssl.credentials); - - /* require at least 512 dh bits */ - gnutls_dh_set_prime_bits(conn->ssl.context, 512); + if (ccode < 0) { + if (conn->ssl.credentials) { + gnutls_certificate_free_credentials(conn->ssl.credentials); + conn->ssl.credentials = NULL; + } + gnutls_deinit(conn->ssl.context); + conn->ssl.context = NULL; + return FALSE; } return TRUE; @@ -101,55 +115,9 @@ ConnStartup(unsigned long TimeOut) /** * Start up the connio library - must be initialised before use */ - FILE *genparams; - /* initialize the gnutls library */ - gnutls_global_init(); - - /* try to load dh data out of a file */ - gnutls_dh_params_init(&dh_params); - genparams = fopen(XPL_DEFAULT_DHPARAMS_PATH, "r"); - if (genparams) { - unsigned char tmpdata[2048]; - gnutls_datum dh_parameters; - - dh_parameters.size = fread(tmpdata, 1, sizeof(tmpdata)-1, genparams); - /* null the last byte */ - tmpdata[dh_parameters.size] = 0x00; - - fclose(genparams); - - /* store the data in the datum so that we can initialize it */ - dh_parameters.data = tmpdata; - - /* import the key */ - gnutls_dh_params_import_pkcs3(dh_params, &dh_parameters, GNUTLS_X509_FMT_PEM); - } else { - /* generate dh stuff */ - gnutls_dh_params_generate2(dh_params, 1024); - } - - /* try to load the rsa data out of a file */ - gnutls_rsa_params_init(&rsa_params); - genparams = fopen(XPL_DEFAULT_RSAPARAMS_PATH, "r"); - if (genparams) { - unsigned char tmpdata[2048]; - gnutls_datum rsa_parameters; - - rsa_parameters.size = fread(tmpdata, 1, sizeof(tmpdata)-1, genparams); - /* null the last byte */ - tmpdata[rsa_parameters.size] = 0x00; - - fclose(genparams); - - /* store the data in the datum so that we can initialize it */ - rsa_parameters.data = tmpdata; - - /* import the key */ - gnutls_rsa_params_import_pkcs1(rsa_params, &rsa_parameters, GNUTLS_X509_FMT_PEM); - } else { - /* generate rsa stuff */ - gnutls_rsa_params_generate2(rsa_params, 512); + if (gnutls_global_init() < 0) { + return FALSE; } XplOpenLocalSemaphore(ConnIO.allocated.sem, 0); @@ -185,7 +153,16 @@ ConnSSLContextAlloc(ConnSSLConfiguration *ConfigSSL) int ccode; /* get me the context */ + if (ConfigSSL == NULL || ConfigSSL->certificate.file == NULL || + ConfigSSL->key.file == NULL) { + return NULL; + } result = MemMalloc(sizeof(bongo_ssl_context)); + if (result == NULL) { + return NULL; + } + memset(result, 0, sizeof(*result)); + result->options = ConfigSSL->options; /* allocate the credential holder */ ccode = gnutls_certificate_allocate_credentials(&(result->cert_cred)); @@ -194,11 +171,22 @@ ConnSSLContextAlloc(ConnSSLConfiguration *ConfigSSL) return(NULL); } - ccode = gnutls_certificate_set_x509_key_file(result->cert_cred, ConfigSSL->certificate.file, ConfigSSL->key.file, ConfigSSL->key.type); + ccode = gnutls_certificate_set_x509_key_file( + result->cert_cred, ConfigSSL->certificate.file, + ConfigSSL->key.file, ConfigSSL->key.type); + if (ccode < 0) { + gnutls_certificate_free_credentials(result->cert_cred); + MemFree(result); + return NULL; + } - /* store both the rsa and dh data in the creds */ - gnutls_certificate_set_dh_params(result->cert_cred, dh_params); - gnutls_certificate_set_rsa_export_params(result->cert_cred, rsa_params); + ccode = gnutls_certificate_set_known_dh_params( + result->cert_cred, GNUTLS_SEC_PARAM_HIGH); + if (ccode < 0) { + gnutls_certificate_free_credentials(result->cert_cred); + MemFree(result); + return NULL; + } return result; } @@ -397,11 +385,11 @@ ConnConnectEx(Connection *conn, struct sockaddr *saddr, socklen_t slen, bongo_ss int ConnEncrypt(Connection *conn, bongo_ssl_context *context) { - int ccode; + int ccode = 1; register Connection *c = conn; if (__gnutls_new(c, context, GNUTLS_CLIENT) != FALSE) { - setsockopt(c->socket, IPPROTO_TCP, 1, (unsigned char *)&ccode, sizeof(ccode)); + setsockopt(c->socket, IPPROTO_TCP, TCP_NODELAY, &ccode, sizeof(ccode)); gnutls_transport_set_ptr(c->ssl.context, (gnutls_transport_ptr_t) (long) c->socket); ccode = gnutls_handshake (c->ssl.context); @@ -424,18 +412,17 @@ ConnEncrypt(Connection *conn, bongo_ssl_context *context) BOOL ConnNegotiate(Connection *conn, bongo_ssl_context *context) { - int ccode; + int ccode = 1; register Connection *c = conn; if (c->ssl.enable == FALSE) { - ccode = 1; - setsockopt(c->socket, IPPROTO_TCP, 1, (unsigned char *)&ccode, sizeof(ccode)); + setsockopt(c->socket, IPPROTO_TCP, TCP_NODELAY, &ccode, sizeof(ccode)); return(TRUE); } if (__gnutls_new(conn, context, GNUTLS_SERVER) != FALSE) { - setsockopt(c->socket, IPPROTO_TCP, 1, (unsigned char *)&ccode, sizeof(ccode)); + setsockopt(c->socket, IPPROTO_TCP, TCP_NODELAY, &ccode, sizeof(ccode)); gnutls_transport_set_ptr(c->ssl.context, (gnutls_transport_ptr_t) (long) c->socket); ccode = gnutls_handshake(c->ssl.context); @@ -571,8 +558,6 @@ ConnShutdown(void) XplCloseLocalSemaphore(ConnIO.allocated.sem); - gnutls_dh_params_deinit(dh_params); - gnutls_rsa_params_deinit(rsa_params); gnutls_global_deinit(); return; @@ -691,7 +676,7 @@ ConnRead(Connection *Conn, char *Dest, int Length) /* bytes read which can only be Count on success. */ int -ConnReadCount(Connection *Conn, char *Dest, int Count) +ConnReadCount(Connection *Conn, void *Dest, int Count) { size_t buffered; size_t remaining = Count; @@ -1473,7 +1458,7 @@ ConnReadToConnUntilEOS(Connection *Src, Connection *Dest) } int -ConnWrite(Connection *Conn, const char *Buffer, int Length) +ConnWrite(Connection *Conn, const void *Buffer, int Length) { const char *b; size_t i; @@ -1719,4 +1704,3 @@ XplPrintIPAddress(char *buffer, int bufLen, unsigned long address) ((address>>24) & 0xff)); #endif } - diff --git a/src/libs/connio/conniop.h b/src/libs/connio/conniop.h index 9a9b03d..b7c4b53 100644 --- a/src/libs/connio/conniop.h +++ b/src/libs/connio/conniop.h @@ -48,7 +48,7 @@ typedef struct _ConnIO { extern ConnIOGlobals ConnIO; -BOOL __gnutls_new(Connection *conn, bongo_ssl_context *context, gnutls_connection_end_t con_end); +BOOL __gnutls_new(Connection *conn, bongo_ssl_context *context, unsigned int flags); long ConnAppendToAllocatedBuffer(const char *source, const long size, char **buffer, unsigned long start_of_buffer, unsigned long *buffersize); diff --git a/src/libs/connio/nmap.c b/src/libs/connio/nmap.c index 2199181..ab71059 100644 --- a/src/libs/connio/nmap.c +++ b/src/libs/connio/nmap.c @@ -34,6 +34,7 @@ #include #include #include +#include /* prototypes */ BOOL NMAPAuthenticate(Connection *conn, char *response, int length); @@ -1300,7 +1301,7 @@ NMAPReadConfigFile(const char *file, char **output) } if (count == 0) { // take into account "empty" files - *output = '\0'; + (*output)[0] = '\0'; } } retcode = TRUE; @@ -1420,7 +1421,7 @@ NMAPConnectQueueEx(char *address, struct sockaddr_in *addr, TraceDestination *de BOOL NMAPEncrypt(Connection *conn, char *response, int length, BOOL force) { - int ccode; + int ccode = 1; BOOL result; Connection *c = conn; @@ -1430,7 +1431,7 @@ NMAPEncrypt(Connection *conn, char *response, int length, BOOL force) } if (NMAPLibrary.context) { - setsockopt(c->socket, IPPROTO_TCP, 1, (unsigned char *)&ccode, sizeof(ccode)); + setsockopt(c->socket, IPPROTO_TCP, TCP_NODELAY, &ccode, sizeof(ccode)); if (((ccode = ConnWrite(c, "CAPA\r\n", 6)) != -1) && ((ccode = ConnFlush(c)) != -1)) { diff --git a/src/libs/json/json.c b/src/libs/json/json.c index 1a62f9c..f050ba7 100644 --- a/src/libs/json/json.c +++ b/src/libs/json/json.c @@ -33,18 +33,19 @@ BongoJsonNode * BongoJsonJPath(BongoJsonNode *root, const char *path) { char rule[50]; - char *select, *nextrule; + char *select; + const char *nextrule; size_t rulelen; BongoJsonResult result; nextrule = strchr(path, '/'); if (! nextrule) { - strncpy(rule, path, 49); - rulelen = 49; + rulelen = min(strlen(path), sizeof(rule) - 1); + memcpy(rule, path, rulelen); } else { - strncpy(rule, path, min(nextrule - path, 49)); - rulelen = nextrule - path; + rulelen = min((size_t)(nextrule - path), sizeof(rule) - 1); + memcpy(rule, path, rulelen); if (rulelen < strlen(path)) { // there's another rule after this nextrule++; @@ -952,4 +953,3 @@ BongoJsonObjectResolve(BongoJsonObject *obj, const char *path, BongoJsonNode **o return res; } - diff --git a/src/libs/json/parse.c b/src/libs/json/parse.c index 7e705d2..8ac2294 100644 --- a/src/libs/json/parse.c +++ b/src/libs/json/parse.c @@ -654,18 +654,18 @@ BongoJsonQuoteStringToStringBuilder(const char *str, BongoStringBuilder *sb) BongoStringBuilderAppendChar(sb, '"'); for (i = 0; i < len; i++) { - int len; + int char_len; c = str[i]; - UTF8_LENGTH(c, len); + UTF8_LENGTH(c, char_len); - if (len < 2) { + if (char_len < 2) { if ((c == '\\') || (c == '"')) { - char str[2]; - str[0] = '\\'; - str[1] = c; - BongoStringBuilderAppendN(sb, str, 2); + char escaped[2]; + escaped[0] = '\\'; + escaped[1] = c; + BongoStringBuilderAppendN(sb, escaped, 2); } else if (c == '\b') { BongoStringBuilderAppend(sb, "\\b"); } else if (c == '\t') { @@ -682,8 +682,8 @@ BongoJsonQuoteStringToStringBuilder(const char *str, BongoStringBuilder *sb) } } } else { - BongoStringBuilderAppendN(sb, str + i, len); - i += len - 1; + BongoStringBuilderAppendN(sb, str + i, char_len); + i += char_len - 1; } } diff --git a/src/libs/mailauth/CMakeLists.txt b/src/libs/mailauth/CMakeLists.txt new file mode 100644 index 0000000..32f815e --- /dev/null +++ b/src/libs/mailauth/CMakeLists.txt @@ -0,0 +1,48 @@ +add_library(bongomailauth + mailauth.c + spf.c + dkim.c + dmarc.c + srs.c +) + +target_include_directories(bongomailauth + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR} + ${PSL_INCLUDE_DIRS} +) + +target_link_libraries(bongomailauth + PUBLIC + SPF2::SPF2 + OpenDKIM::OpenDKIM + OpenDMARC::OpenDMARC + SRS2::SRS2 + ${PSL_LIBRARIES} +) + +install(TARGETS bongomailauth DESTINATION ${LIB_INSTALL_DIR}) + +if(BUILD_TESTING) + add_executable(mailauth-srs-test tests/srs-test.c) + target_link_libraries(mailauth-srs-test PRIVATE bongomailauth) + add_test(NAME mailauth-srs COMMAND mailauth-srs-test) + + add_executable(mailauth-spf-test tests/spf-test.c) + target_link_libraries(mailauth-spf-test PRIVATE bongomailauth) + add_test(NAME mailauth-spf COMMAND mailauth-spf-test) + + find_program(OPENSSL_EXECUTABLE openssl) + if(OPENSSL_EXECUTABLE) + set(DKIM_TEST_KEY "${CMAKE_CURRENT_BINARY_DIR}/dkim-test.private") + add_test(NAME mailauth-dkim-key + COMMAND ${OPENSSL_EXECUTABLE} genpkey -quiet -algorithm RSA + -pkeyopt rsa_keygen_bits:2048 -out ${DKIM_TEST_KEY}) + set_tests_properties(mailauth-dkim-key PROPERTIES FIXTURES_SETUP dkim_key) + + add_executable(mailauth-dkim-test tests/dkim-test.c) + target_link_libraries(mailauth-dkim-test PRIVATE bongomailauth) + add_test(NAME mailauth-dkim COMMAND mailauth-dkim-test ${DKIM_TEST_KEY}) + set_tests_properties(mailauth-dkim PROPERTIES FIXTURES_REQUIRED dkim_key) + endif() +endif() diff --git a/src/libs/mailauth/dkim.c b/src/libs/mailauth/dkim.c new file mode 100644 index 0000000..14cf485 --- /dev/null +++ b/src/libs/mailauth/dkim.c @@ -0,0 +1,155 @@ +#include + +#include +#include +#include +#include + +#include "mailauth.h" + +#define BONGO_DKIM_KEY_MAX (1024U * 1024U) + +struct BongoDKIMSigner { + DKIM_LIB *library; + DKIM *context; + unsigned char *key; +}; + +static BongoMailAuthStatus +ReadPrivateKey(const char *path, unsigned char **result) +{ + struct stat metadata; + FILE *file; + unsigned char *key; + size_t length; + + if (path == NULL || result == NULL || stat(path, &metadata) != 0 || + !S_ISREG(metadata.st_mode) || metadata.st_size <= 0 || + (uintmax_t) metadata.st_size > BONGO_DKIM_KEY_MAX) { + return BONGO_MAILAUTH_IO_ERROR; + } + if ((metadata.st_mode & S_IRWXO) != 0) { + return BONGO_MAILAUTH_INSECURE_SECRET; + } + file = fopen(path, "rb"); + if (file == NULL) { + return BONGO_MAILAUTH_IO_ERROR; + } + length = (size_t) metadata.st_size; + key = malloc(length + 1U); + if (key == NULL) { + fclose(file); + return BONGO_MAILAUTH_NO_MEMORY; + } + if (fread(key, 1U, length, file) != length || fclose(file) != 0) { + memset(key, 0, length); + free(key); + return BONGO_MAILAUTH_IO_ERROR; + } + key[length] = '\0'; + *result = key; + return BONGO_MAILAUTH_OK; +} + +int +BongoMailAuthDKIMAvailable(void) +{ + return OPENDKIM_LIB_VERSION >= 0; +} + +BongoMailAuthStatus +BongoMailAuthDKIMSignerCreate(const char *key_file, const char *selector, + const char *domain, const char *message_id, + BongoDKIMSigner **result) +{ + BongoDKIMSigner *signer; + BongoMailAuthStatus status; + DKIM_STAT dkimStatus; + + if (selector == NULL || selector[0] == '\0' || domain == NULL || + domain[0] == '\0' || message_id == NULL || message_id[0] == '\0' || + result == NULL) { + return BONGO_MAILAUTH_INVALID_ARGUMENT; + } + *result = NULL; + signer = calloc(1U, sizeof(*signer)); + if (signer == NULL) { + return BONGO_MAILAUTH_NO_MEMORY; + } + status = ReadPrivateKey(key_file, &signer->key); + if (status != BONGO_MAILAUTH_OK) { + free(signer); + return status; + } + signer->library = dkim_init(NULL, NULL); + if (signer->library == NULL) { + BongoMailAuthDKIMSignerFree(signer); + return BONGO_MAILAUTH_NO_MEMORY; + } + signer->context = dkim_sign( + signer->library, (const unsigned char *) message_id, NULL, + signer->key, (const unsigned char *) selector, + (const unsigned char *) domain, DKIM_CANON_RELAXED, + DKIM_CANON_RELAXED, DKIM_SIGN_RSASHA256, -1, &dkimStatus); + if (signer->context == NULL || dkimStatus != DKIM_STAT_OK) { + BongoMailAuthDKIMSignerFree(signer); + return BONGO_MAILAUTH_PROTOCOL_ERROR; + } + *result = signer; + return BONGO_MAILAUTH_OK; +} + +BongoMailAuthStatus +BongoMailAuthDKIMSignerUpdate(BongoDKIMSigner *signer, const void *data, + size_t length) +{ + if (signer == NULL || signer->context == NULL || + (data == NULL && length != 0U)) { + return BONGO_MAILAUTH_INVALID_ARGUMENT; + } + return dkim_chunk(signer->context, (unsigned char *) data, length) == + DKIM_STAT_OK + ? BONGO_MAILAUTH_OK : BONGO_MAILAUTH_PROTOCOL_ERROR; +} + +BongoMailAuthStatus +BongoMailAuthDKIMSignerFinish(BongoDKIMSigner *signer, char *signature, + size_t signature_size) +{ + _Bool testKey = 0; + + if (signer == NULL || signer->context == NULL || signature == NULL || + signature_size < 2U) { + return BONGO_MAILAUTH_INVALID_ARGUMENT; + } + if (dkim_chunk(signer->context, NULL, 0U) != DKIM_STAT_OK || + dkim_eom(signer->context, &testKey) != DKIM_STAT_OK || + dkim_getsighdr(signer->context, (unsigned char *) signature, + signature_size, strlen("DKIM-Signature: ")) != + DKIM_STAT_OK) { + return BONGO_MAILAUTH_PROTOCOL_ERROR; + } + signature[signature_size - 1U] = '\0'; + return BONGO_MAILAUTH_OK; +} + +void +BongoMailAuthDKIMSignerFree(BongoDKIMSigner *signer) +{ + if (signer == NULL) { + return; + } + if (signer->context != NULL) { + dkim_free(signer->context); + } + if (signer->library != NULL) { + dkim_close(signer->library); + } + if (signer->key != NULL) { + size_t length = strlen((const char *) signer->key); + memset(signer->key, 0, length); + free(signer->key); + } + memset(signer, 0, sizeof(*signer)); + free(signer); +} diff --git a/src/libs/mailauth/dmarc.c b/src/libs/mailauth/dmarc.c new file mode 100644 index 0000000..012f6ad --- /dev/null +++ b/src/libs/mailauth/dmarc.c @@ -0,0 +1,7 @@ +#include + +int +BongoMailAuthDMARCAvailable(void) +{ + return DMARC_POLICY_SPF_ORIGIN_MAILFROM >= 0; +} diff --git a/src/libs/mailauth/mailauth.c b/src/libs/mailauth/mailauth.c new file mode 100644 index 0000000..31d24b2 --- /dev/null +++ b/src/libs/mailauth/mailauth.c @@ -0,0 +1,19 @@ +#include "mailauth.h" + +int BongoMailAuthSPFAvailable(void); +int BongoMailAuthDKIMAvailable(void); +int BongoMailAuthDMARCAvailable(void); +int BongoMailAuthSRSAvailable(void); + +BongoMailAuthCapabilities +BongoMailAuthGetCapabilities(void) +{ + BongoMailAuthCapabilities capabilities = { + (unsigned int) BongoMailAuthSPFAvailable(), + (unsigned int) BongoMailAuthDKIMAvailable(), + (unsigned int) BongoMailAuthDMARCAvailable(), + (unsigned int) BongoMailAuthSRSAvailable() + }; + + return capabilities; +} diff --git a/src/libs/mailauth/mailauth.h b/src/libs/mailauth/mailauth.h new file mode 100644 index 0000000..d79e2d3 --- /dev/null +++ b/src/libs/mailauth/mailauth.h @@ -0,0 +1,77 @@ +#ifndef BONGO_MAILAUTH_H +#define BONGO_MAILAUTH_H + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct BongoMailAuthCapabilities { + unsigned int spf; + unsigned int dkim; + unsigned int dmarc; + unsigned int srs; +} BongoMailAuthCapabilities; + +/* Reports the protocol engines compiled into this Bongo build. */ +BongoMailAuthCapabilities BongoMailAuthGetCapabilities(void); + +typedef enum BongoMailAuthStatus { + BONGO_MAILAUTH_OK = 0, + BONGO_MAILAUTH_INVALID_ARGUMENT, + BONGO_MAILAUTH_NO_MEMORY, + BONGO_MAILAUTH_IO_ERROR, + BONGO_MAILAUTH_INSECURE_SECRET, + BONGO_MAILAUTH_PROTOCOL_ERROR +} BongoMailAuthStatus; + +typedef enum BongoSPFResult { + BONGO_SPF_INVALID = 0, + BONGO_SPF_NEUTRAL, + BONGO_SPF_PASS, + BONGO_SPF_FAIL, + BONGO_SPF_SOFTFAIL, + BONGO_SPF_NONE, + BONGO_SPF_TEMPERROR, + BONGO_SPF_PERMERROR +} BongoSPFResult; + +typedef struct BongoSPFResponse { + BongoSPFResult result; + char smtp_comment[512]; + char received_spf[1024]; +} BongoSPFResponse; + +BongoMailAuthStatus BongoMailAuthCheckSPF(const char *receiver_domain, + const char *client_ip, + const char *helo_domain, + const char *mail_from, + BongoSPFResponse *response); + +BongoMailAuthStatus BongoMailAuthSRSForward(const char *secret_file, + const char *srs_domain, + const char *sender, + char *output, + size_t output_size); +BongoMailAuthStatus BongoMailAuthSRSReverse(const char *secret_file, + const char *sender, + char *output, + size_t output_size); + +typedef struct BongoDKIMSigner BongoDKIMSigner; + +BongoMailAuthStatus BongoMailAuthDKIMSignerCreate( + const char *key_file, const char *selector, const char *domain, + const char *message_id, BongoDKIMSigner **signer); +BongoMailAuthStatus BongoMailAuthDKIMSignerUpdate( + BongoDKIMSigner *signer, const void *data, size_t length); +BongoMailAuthStatus BongoMailAuthDKIMSignerFinish( + BongoDKIMSigner *signer, char *signature, size_t signature_size); +void BongoMailAuthDKIMSignerFree(BongoDKIMSigner *signer); + +#ifdef __cplusplus +} +#endif + +#endif diff --git a/src/libs/mailauth/spf.c b/src/libs/mailauth/spf.c new file mode 100644 index 0000000..a983728 --- /dev/null +++ b/src/libs/mailauth/spf.c @@ -0,0 +1,91 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "mailauth.h" + +static void +CopyString(char *destination, size_t destination_size, const char *source) +{ + if (destination_size == 0U) { + return; + } + snprintf(destination, destination_size, "%s", source != NULL ? source : ""); +} + +int +BongoMailAuthSPFAvailable(void) +{ + return SPF_LIB_VERSION_MAJOR >= 0; +} + +BongoMailAuthStatus +BongoMailAuthCheckSPF(const char *receiver_domain, const char *client_ip, + const char *helo_domain, const char *mail_from, + BongoSPFResponse *response) +{ + SPF_server_t *server = NULL; + SPF_request_t *request = NULL; + SPF_response_t *spf_response = NULL; + SPF_errcode_t error; + struct in_addr ipv4; + struct in6_addr ipv6; + BongoMailAuthStatus status = BONGO_MAILAUTH_PROTOCOL_ERROR; + + if (receiver_domain == NULL || receiver_domain[0] == '\0' || + client_ip == NULL || client_ip[0] == '\0' || + helo_domain == NULL || helo_domain[0] == '\0' || + mail_from == NULL || response == NULL) { + return BONGO_MAILAUTH_INVALID_ARGUMENT; + } + memset(response, 0, sizeof(*response)); + + if (inet_pton(AF_INET, client_ip, &ipv4) != 1 && + inet_pton(AF_INET6, client_ip, &ipv6) != 1) { + return BONGO_MAILAUTH_INVALID_ARGUMENT; + } + + server = SPF_server_new(SPF_DNS_CACHE, 0); + if (server == NULL) { + return BONGO_MAILAUTH_NO_MEMORY; + } + if (SPF_server_set_rec_dom(server, receiver_domain) != SPF_E_SUCCESS) { + goto cleanup; + } + + request = SPF_request_new(server); + if (request == NULL) { + status = BONGO_MAILAUTH_NO_MEMORY; + goto cleanup; + } + error = strchr(client_ip, ':') != NULL + ? SPF_request_set_ipv6_str(request, client_ip) + : SPF_request_set_ipv4_str(request, client_ip); + if (error != SPF_E_SUCCESS || + SPF_request_set_helo_dom(request, helo_domain) != SPF_E_SUCCESS || + SPF_request_set_env_from(request, mail_from) != SPF_E_SUCCESS) { + status = BONGO_MAILAUTH_INVALID_ARGUMENT; + goto cleanup; + } + + error = SPF_request_query_mailfrom(request, &spf_response); + if (spf_response == NULL) { + goto cleanup; + } + response->result = (BongoSPFResult) SPF_response_result(spf_response); + CopyString(response->smtp_comment, sizeof(response->smtp_comment), + SPF_response_get_smtp_comment(spf_response)); + CopyString(response->received_spf, sizeof(response->received_spf), + SPF_response_get_received_spf(spf_response)); + status = error == SPF_E_SUCCESS ? BONGO_MAILAUTH_OK : BONGO_MAILAUTH_PROTOCOL_ERROR; + +cleanup: + SPF_response_free(spf_response); + SPF_request_free(request); + SPF_server_free(server); + return status; +} diff --git a/src/libs/mailauth/srs.c b/src/libs/mailauth/srs.c new file mode 100644 index 0000000..bf86403 --- /dev/null +++ b/src/libs/mailauth/srs.c @@ -0,0 +1,123 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "mailauth.h" + +#define BONGO_SRS_SECRET_MAX 1024U +#define BONGO_SRS_SECRET_MIN 16U + +static BongoMailAuthStatus +LoadSecrets(srs_t *srs, const char *path) +{ + struct stat metadata; + FILE *file; + char line[BONGO_SRS_SECRET_MAX + 2U]; + unsigned int count = 0U; + + if (stat(path, &metadata) != 0 || !S_ISREG(metadata.st_mode)) { + return BONGO_MAILAUTH_IO_ERROR; + } + if ((metadata.st_mode & S_IRWXO) != 0) { + return BONGO_MAILAUTH_INSECURE_SECRET; + } + file = fopen(path, "r"); + if (file == NULL) { + return BONGO_MAILAUTH_IO_ERROR; + } + while (fgets(line, sizeof(line), file) != NULL) { + size_t length = strcspn(line, "\r\n"); + line[length] = '\0'; + if (length == 0U || line[0] == '#') { + continue; + } + if (length < BONGO_SRS_SECRET_MIN || length > BONGO_SRS_SECRET_MAX || + srs_add_secret(srs, line) != SRS_SUCCESS) { + memset(line, 0, sizeof(line)); + fclose(file); + return BONGO_MAILAUTH_PROTOCOL_ERROR; + } + memset(line, 0, sizeof(line)); + count++; + } + if (ferror(file) != 0 || fclose(file) != 0) { + return BONGO_MAILAUTH_IO_ERROR; + } + return count > 0U ? BONGO_MAILAUTH_OK : BONGO_MAILAUTH_PROTOCOL_ERROR; +} + +static BongoMailAuthStatus +CreateSRS(srs_t **result, const char *secret_file) +{ + BongoMailAuthStatus status; + srs_t *srs; + + if (result == NULL || secret_file == NULL || secret_file[0] == '\0') { + return BONGO_MAILAUTH_INVALID_ARGUMENT; + } + srs = srs_new(); + if (srs == NULL) { + return BONGO_MAILAUTH_NO_MEMORY; + } + status = LoadSecrets(srs, secret_file); + if (status != BONGO_MAILAUTH_OK) { + srs_free(srs); + return status; + } + *result = srs; + return BONGO_MAILAUTH_OK; +} + +int +BongoMailAuthSRSAvailable(void) +{ + return SRS_SUCCESS == 0; +} + +BongoMailAuthStatus +BongoMailAuthSRSForward(const char *secret_file, const char *srs_domain, + const char *sender, char *output, size_t output_size) +{ + BongoMailAuthStatus status; + srs_t *srs = NULL; + int result; + + if (srs_domain == NULL || srs_domain[0] == '\0' || sender == NULL || + sender[0] == '\0' || output == NULL || output_size < 2U || + output_size > (size_t) INT_MAX) { + return BONGO_MAILAUTH_INVALID_ARGUMENT; + } + status = CreateSRS(&srs, secret_file); + if (status != BONGO_MAILAUTH_OK) { + return status; + } + result = srs_forward(srs, output, (int) output_size, sender, srs_domain); + srs_free(srs); + return result == SRS_SUCCESS ? BONGO_MAILAUTH_OK : BONGO_MAILAUTH_PROTOCOL_ERROR; +} + +BongoMailAuthStatus +BongoMailAuthSRSReverse(const char *secret_file, const char *sender, + char *output, size_t output_size) +{ + BongoMailAuthStatus status; + srs_t *srs = NULL; + int result; + + if (sender == NULL || sender[0] == '\0' || output == NULL || + output_size < 2U || output_size > (size_t) INT_MAX) { + return BONGO_MAILAUTH_INVALID_ARGUMENT; + } + status = CreateSRS(&srs, secret_file); + if (status != BONGO_MAILAUTH_OK) { + return status; + } + result = srs_reverse(srs, output, (int) output_size, sender); + srs_free(srs); + return result == SRS_SUCCESS ? BONGO_MAILAUTH_OK : BONGO_MAILAUTH_PROTOCOL_ERROR; +} diff --git a/src/libs/mailauth/tests/dkim-test.c b/src/libs/mailauth/tests/dkim-test.c new file mode 100644 index 0000000..d3801ea --- /dev/null +++ b/src/libs/mailauth/tests/dkim-test.c @@ -0,0 +1,41 @@ +#include "mailauth.h" + +#include +#include +#include + +int +main(int argc, char **argv) +{ + static const char message[] = + "From: sender@example.org\r\n" + "To: recipient@example.net\r\n" + "Subject: DKIM test\r\n" + "Date: Tue, 15 Jul 2026 12:00:00 +0200\r\n" + "Message-ID: \r\n" + "\r\n" + "Test body.\r\n"; + BongoDKIMSigner *signer = NULL; + char signature[8192]; + + if (argc != 2 || chmod(argv[1], S_IRUSR | S_IWUSR) != 0) { + return 1; + } + if (BongoMailAuthDKIMSignerCreate(argv[1], "test", "example.org", + "dkim-test", &signer) != + BONGO_MAILAUTH_OK || + BongoMailAuthDKIMSignerUpdate(signer, message, + sizeof(message) - 1U) != + BONGO_MAILAUTH_OK || + BongoMailAuthDKIMSignerFinish(signer, signature, + sizeof(signature)) != + BONGO_MAILAUTH_OK) { + BongoMailAuthDKIMSignerFree(signer); + return 2; + } + BongoMailAuthDKIMSignerFree(signer); + return strstr(signature, "v=1") != NULL && + strstr(signature, "d=example.org") != NULL && + strstr(signature, "s=test") != NULL + ? 0 : 3; +} diff --git a/src/libs/mailauth/tests/spf-test.c b/src/libs/mailauth/tests/spf-test.c new file mode 100644 index 0000000..2395fb0 --- /dev/null +++ b/src/libs/mailauth/tests/spf-test.c @@ -0,0 +1,12 @@ +#include "mailauth.h" + +int +main(void) +{ + BongoSPFResponse response; + + return BongoMailAuthCheckSPF("receiver.example", "not-an-ip", + "sender.example", "user@sender.example", + &response) == BONGO_MAILAUTH_INVALID_ARGUMENT + ? 0 : 1; +} diff --git a/src/libs/mailauth/tests/srs-test.c b/src/libs/mailauth/tests/srs-test.c new file mode 100644 index 0000000..9648316 --- /dev/null +++ b/src/libs/mailauth/tests/srs-test.c @@ -0,0 +1,37 @@ +#include "mailauth.h" + +#include +#include +#include +#include +#include + +int +main(void) +{ + char path[] = "/tmp/bongo-srs-test-XXXXXX"; + char encoded[1024]; + char decoded[1024]; + const char sender[] = "sender@example.org"; + int descriptor = mkstemp(path); + FILE *file; + + if (descriptor < 0 || fchmod(descriptor, S_IRUSR | S_IWUSR) != 0) { + return 1; + } + file = fdopen(descriptor, "w"); + if (file == NULL || fputs("test-secret-with-at-least-32-bytes\n", file) < 0 || + fclose(file) != 0) { + unlink(path); + return 2; + } + if (BongoMailAuthSRSForward(path, "forward.example.net", sender, + encoded, sizeof(encoded)) != BONGO_MAILAUTH_OK || + BongoMailAuthSRSReverse(path, encoded, decoded, + sizeof(decoded)) != BONGO_MAILAUTH_OK) { + unlink(path); + return 3; + } + unlink(path); + return strcmp(sender, decoded) == 0 ? 0 : 4; +} diff --git a/src/libs/msgapi/auth-backends/sqlite.c b/src/libs/msgapi/auth-backends/sqlite.c index dffb5ab..b75bef8 100644 --- a/src/libs/msgapi/auth-backends/sqlite.c +++ b/src/libs/msgapi/auth-backends/sqlite.c @@ -268,11 +268,12 @@ AuthSqlite_SetPassword(const char *user, const char *password) int AuthSqlite_GetUserStore(const char *user, struct sockaddr_in *store) { - memset(store, 0, sizeof(store)); + UNUSED_PARAMETER(user); + memset(store, 0, sizeof(*store)); store->sin_addr.s_addr = inet_addr("127.0.0.1"); store->sin_family = AF_INET; store->sin_port = htons(689); - return TRUE; + return 0; } int @@ -333,6 +334,8 @@ AuthSqlite_InterfaceVersion(void) int main (int argc, char *argv[]) { + UNUSED_PARAMETER(argc); + UNUSED_PARAMETER(argv); printf("This cannot be run directly"); return (-1); } diff --git a/src/libs/msgapi/msgaddr.c b/src/libs/msgapi/msgaddr.c index 3ef02cb..f0da8b3 100644 --- a/src/libs/msgapi/msgaddr.c +++ b/src/libs/msgapi/msgaddr.c @@ -399,9 +399,10 @@ MsgIsComment(unsigned char *base, unsigned char *limit, unsigned char **out) } EXPORT BOOL -MsgParseAddress(unsigned char *addressLine, size_t addressLineLength, unsigned char **local_part, unsigned char **domain ) { +MsgParseAddress(char *addressLine, size_t addressLineLength, char **local_part, char **domain ) { + UNUSED_PARAMETER(addressLineLength); /*TODO: this is a terribly simple parser of an email address into the local_part and the domain. this needs to be better */ - unsigned char *at = strchr(addressLine, '@'); + char *at = strchr(addressLine, '@'); if (at) { *at = '\0'; (*local_part)= MemStrdup(addressLine); diff --git a/src/libs/msgapi/msgapi.c b/src/libs/msgapi/msgapi.c index 3e70d25..56aaecc 100644 --- a/src/libs/msgapi/msgapi.c +++ b/src/libs/msgapi/msgapi.c @@ -42,6 +42,8 @@ #include #include #include +#include +#include #define MSGAPI_FLAG_EXITING (1 << 0) #define MSGAPI_FLAG_STANDALONE (1 << 1) @@ -93,7 +95,7 @@ struct { struct { unsigned long local; - unsigned char string[16]; + char string[INET_ADDRSTRLEN]; } address; struct { @@ -123,24 +125,28 @@ MsgGetUpdateStatus(char *record, int record_length); void MsgNmapChallenge(const unsigned char *response, unsigned char *reply, size_t length) { - unsigned char *ptr; - unsigned char *salt; + const unsigned char *start; + const unsigned char *end; static char access[NMAP_HASH_SIZE] = { '\0' }; xpl_hash_context ctx; MsgGetServerCredential(access); - if (access[0] && reply && (length > 32) && ((ptr = strchr(response, '<')) != NULL)) { - salt = ++ptr; - - if ((ptr = strchr(ptr, '>')) != NULL) { - *ptr = '\0'; - } - + start = response + ? (const unsigned char *)strchr((const char *)response, '<') + : NULL; + if (access[0] && reply && (length > 32) && start != NULL) { + start++; + end = (const unsigned char *)strchr((const char *)start, '>'); + if (end == NULL) { + *reply = '\0'; + return; + } + XplHashNew(&ctx, XPLHASH_MD5); - XplHashWrite(&ctx, salt, strlen(salt)); + XplHashWrite(&ctx, start, (size_t)(end - start)); XplHashWrite(&ctx, access, NMAP_HASH_SIZE); - XplHashFinal(&ctx, XPLHASH_LOWERCASE, reply, XPLHASH_MD5_LENGTH); + XplHashFinal(&ctx, XPLHASH_LOWERCASE, (char *)reply, XPLHASH_MD5_LENGTH); } else if (reply) { *reply = '\0'; } @@ -151,6 +157,7 @@ MsgNmapChallenge(const unsigned char *response, unsigned char *reply, size_t len EXPORT const unsigned char * MsgGetServerDN(unsigned char *buffer) { + UNUSED_PARAMETER(buffer); // DEPRECATED return(NULL); } @@ -158,6 +165,8 @@ MsgGetServerDN(unsigned char *buffer) EXPORT BOOL MsgDomainExists(const unsigned char *domain, unsigned char *domainObjectDN) { + UNUSED_PARAMETER(domain); + UNUSED_PARAMETER(domainObjectDN); // DEPRECATED ? /* FIXME later; when we handle parent objects */ return(FALSE); @@ -308,7 +317,7 @@ MsgGetDir(MsgApiDirectory directory, char *buffer, size_t buffer_size) EXPORT const char * MsgGetFile(MsgApiFile file, char *buffer, size_t buffer_size) { - const unsigned char *path; + const char *path; switch(file) { case MSGAPI_FILE_PUBKEY: path = XPL_DEFAULT_CERT_PATH; @@ -374,7 +383,6 @@ MsgLibraryStop(void) return(TRUE); } -#if 0 /** * Get a config property from global configuration file. Removes dependency * on NCPServer object in the directory (as well as others). See msgapi.h for @@ -383,44 +391,53 @@ MsgLibraryStop(void) BOOL MsgGetConfigProperty(unsigned char *Buffer, unsigned char *Property) { - int result = FALSE; - char line[512]; - char *ptr; - FILE *fh; - int len; - char conf_path[FILENAME_MAX]; + BOOL result = FALSE; + char line[MSGSRV_CONFIG_MAX_PROP_CHARS * 2 + 4]; + char *name; + char *value; + char *end; + FILE *fh; + char conf_path[FILENAME_MAX]; - snprintf(conf_path, FILENAME_MAX, "%s/bongo.conf", XPL_DEFAULT_CONF_DIR); + if (Buffer == NULL || Property == NULL) + return FALSE; + Buffer[0] = '\0'; + if (snprintf(conf_path, sizeof(conf_path), "%s/%s", XPL_DEFAULT_CONF_DIR, + MSGSRV_CONFIG_FILENAME) >= (int)sizeof(conf_path)) + return FALSE; if (!(fh = fopen(conf_path, "r"))) { return result; } - while (fgets(line, 512, fh)) { - if (line[0] != '#') { - if ((ptr = strchr(line, '='))) { - *ptr++ = '\0'; - len = strlen(ptr); - - if (!strcmp(line, Property)) { - if (ptr[len - 1] == '\n') { - ptr[len - 1] = '\0'; - } - - strncpy(Buffer, ptr, MSGSRV_CONFIG_MAX_PROP_CHARS); - result = TRUE; - break; - } - } else { - break; - } - } - } + while (fgets(line, sizeof(line), fh)) { + name = line; + while (isspace((unsigned char)*name)) + name++; + if (*name == '#' || *name == '\0') + continue; + value = strchr(name, '='); + if (value == NULL) + continue; + *value++ = '\0'; + end = name + strlen(name); + while (end > name && isspace((unsigned char)end[-1])) + *--end = '\0'; + if (strcmp(name, (const char *)Property) != 0) + continue; + while (isspace((unsigned char)*value)) + value++; + end = value + strlen(value); + while (end > value && isspace((unsigned char)end[-1])) + *--end = '\0'; + snprintf((char *)Buffer, MSGSRV_CONFIG_MAX_PROP_CHARS + 1, "%s", value); + result = TRUE; + break; + } fclose(fh); return result; } -#endif void MsgGetUid(char *buffer, int buflen) @@ -452,10 +469,7 @@ MsgGetUid(char *buffer, int buflen) static BOOL MsgLibraryStart(void) { - unsigned long i; struct sockaddr_in server_sockaddr; - unsigned char path[XPL_MAX_PATH + 1]; - XplThreadID ID; /* Prepare later config updates */ XplRWLockInit(&MsgGlobal.configLock); @@ -468,12 +482,13 @@ MsgLibraryStart(void) /* set the ip address up correctly */ server_sockaddr.sin_addr.s_addr = XplGetHostIPAddress(); - sprintf(MsgGlobal.address.string, "%d.%d.%d.%d", - server_sockaddr.sin_addr.s_net, - server_sockaddr.sin_addr.s_host, - server_sockaddr.sin_addr.s_lh, - server_sockaddr.sin_addr.s_impno); - MsgGlobal.address.local = inet_addr(MsgGlobal.address.string); + if (inet_ntop(AF_INET, &server_sockaddr.sin_addr, + MsgGlobal.address.string, + sizeof(MsgGlobal.address.string)) == NULL) { + strcpy(MsgGlobal.address.string, "127.0.0.1"); + server_sockaddr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + } + MsgGlobal.address.local = server_sockaddr.sin_addr.s_addr; return(TRUE); } @@ -504,8 +519,6 @@ MsgGetAvailableVersion(int *version) */ char record[500]; char *ptr; - int v; - if (!MsgGetUpdateStatus(record, 499)) return FALSE; @@ -542,7 +555,6 @@ MsgGetUpdateStatus(char *record, int record_length) static BOOL MsgReadConfiguration(void) { - unsigned long i; struct tm *timeStruct; time_t utcTime; long tmp; @@ -609,6 +621,7 @@ MsgLibraryShutdown(void) MsgGlobal.flags |= MSGAPI_FLAG_EXITING; oldGid = XplSetThreadGroupID(MsgGlobal.groupID); + UNUSED_PARAMETER(oldGid); MsgLibraryStop(); @@ -717,8 +730,8 @@ MsgClearRecoveryFlag(char *agent_name) EXPORT BOOL MsgGetServerCredential(char *buffer) { - unsigned char credential[4097]; - unsigned char file[120]; + char credential[4097]; + char file[120]; FILE *credfile; memset(credential, 0, sizeof(credential)); @@ -738,9 +751,10 @@ MsgGetServerCredential(char *buffer) EXPORT BOOL MsgSetServerCredential() { - unsigned char credential[4097]; - unsigned char path[XPL_MAX_PATH]; + char credential[4097]; + char path[XPL_MAX_PATH]; FILE *credfile; + int fd; const char *posschars = "abcdefghijlkmnopqrstuvwxyz" "ABCDEFGHIJKLMNOPQRSTUVWYXZ" @@ -748,18 +762,26 @@ MsgSetServerCredential() int i, range; range = strlen(posschars); - for (i=0; i<4097; i++) { - int ran = rand() % range; - credential[i] = posschars[ran]; + XplRandomData(credential, sizeof(credential) - 1); + for (i = 0; i < (int)sizeof(credential) - 1; i++) { + credential[i] = posschars[(unsigned char)credential[i] % range]; } credential[4096] = '\0'; snprintf(path, XPL_MAX_PATH, "%s/credential.dat", XPL_DEFAULT_DBF_DIR); - credfile = fopen(path, "wb"); + fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, S_IRUSR | S_IWUSR); + if (fd >= 0 && fchmod(fd, S_IRUSR | S_IWUSR) != 0) { + close(fd); + fd = -1; + } + credfile = fd >= 0 ? fdopen(fd, "wb") : NULL; if (credfile) { - fwrite(credential, sizeof(char), sizeof(credential), credfile); - fclose(credfile); - return TRUE; + BOOL ok = fwrite(credential, sizeof(char), sizeof(credential), credfile) == sizeof(credential); + ok = (fclose(credfile) == 0) && ok; + return ok; + } + if (fd >= 0) { + close(fd); } return FALSE; } diff --git a/src/libs/msgapi/msgauth.c b/src/libs/msgapi/msgauth.c index 8427967..570ef3f 100644 --- a/src/libs/msgapi/msgauth.c +++ b/src/libs/msgapi/msgauth.c @@ -249,7 +249,7 @@ MsgAuthGetUserStore(const char *user, struct sockaddr_in *store) f = (AuthPlugin_GetUserStore *)&function->func; result = (*f)(user, store); } else { - memset(store, 0, sizeof(store)); + memset(store, 0, sizeof(*store)); store->sin_addr.s_addr = inet_addr("127.0.0.1"); store->sin_family = AF_INET; store->sin_port = htons(689); diff --git a/src/libs/msgapi/msgcollector.c b/src/libs/msgapi/msgcollector.c index e79ea6b..9c04076 100644 --- a/src/libs/msgapi/msgcollector.c +++ b/src/libs/msgapi/msgcollector.c @@ -129,7 +129,6 @@ static Connection * ConnectToStore(const char *user) { char buffer[CONN_BUFSIZE + 1]; - struct sockaddr_in addr; Connection *conn; int ret; @@ -304,10 +303,10 @@ RemoveGuidForeach (void *key, void *value, void *data) RemoveGuidForeachData *rgfd = data; EventData *evtData = value; - int ccode; + UNUSED_PARAMETER(key); NMAPSendCommandF(rgfd->conn, "UNLINK /calendars/%s %s\r\n", rgfd->calendarName, evtData->guid); - ccode = NMAPReadAnswer(rgfd->conn, rgfd->buffer, rgfd->bufferLen, TRUE); + NMAPReadAnswer(rgfd->conn, rgfd->buffer, rgfd->bufferLen, TRUE); } static int @@ -427,6 +426,8 @@ MsgImportIcs(FILE *fh, int ret; const char *icsName; char *icsNameDup = NULL; + + UNUSED_PARAMETER(calendarColor); component = ParseIcs(fh); if (!component) { @@ -701,7 +702,7 @@ MsgImportIcsUrl(const char *user, FILE *fh = XplOpenTemp("/tmp", "w+", filename); char cred[1024]; - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, TRUE); + curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); if (aliasedUrl) { curl_easy_setopt(curl, CURLOPT_URL, aliasedUrl); } else { diff --git a/src/libs/msgapi/msgdate.c b/src/libs/msgapi/msgdate.c index 762d28a..3b68306 100644 --- a/src/libs/msgapi/msgdate.c +++ b/src/libs/msgapi/msgdate.c @@ -176,10 +176,10 @@ MsgGetDate(long rataDie, unsigned long *day, unsigned long *month, unsigned long } EXPORT BOOL -MsgGetRFC822Date(long offset, long utcTime, unsigned char *dateBuffer) +MsgGetRFC822Date(long offset, long utcTime, char *dateBuffer) { struct tm timeStruct; - unsigned char tempBuffer[80]; + char tempBuffer[80]; if (utcTime == 0) { utcTime = time(NULL); @@ -263,16 +263,22 @@ uint64_t MsgParseRFC822Date(const char *date) { uint64_t utc; + char *copy; char *dayPtr; char *monthPtr; char *yearPtr; unsigned long month; - dayPtr = strchr(date, ','); + copy = MemStrdup(date); + if (copy == NULL) { + return 0; + } + + dayPtr = strchr(copy, ','); if (dayPtr) { dayPtr++; } else { - dayPtr = (char *)date; + dayPtr = copy; } while((*dayPtr == ' ') || (*dayPtr == '\t')) { @@ -290,11 +296,13 @@ MsgParseRFC822Date(const char *date) yearPtr++; utc = MsgGetUTC((unsigned long)atol(dayPtr), month, (unsigned long)atol(yearPtr), 0, 0, 0); if (utc < ((unsigned long) -1)) { + MemFree(copy); return(utc); } } } + MemFree(copy); return 0; } @@ -463,22 +471,43 @@ MsgGetDMY(unsigned long utcTime, unsigned long *day, unsigned long *month, unsig * %Y - Year with century as decimal number * %Z - Timezone offset from UTC (+/- 0000) */ -EXPORT unsigned long -MsgPrint(unsigned char *buffer, int bufSize, unsigned char *format, unsigned long timeIn, MsgDateFormat *dateInfo) +static void +MsgAppendFormat(char **dest, char *limit, const char *format, ...) { - unsigned long rataDie; - unsigned char *src; - unsigned char *dest; - unsigned char *limit; + size_t available; + int written; + va_list args; + + if (*dest >= limit) { + return; + } + available = (size_t)(limit - *dest); + va_start(args, format); + written = vsnprintf(*dest, available, format, args); + va_end(args); + if (written < 0 || (size_t)written >= available) { + *dest = limit; + } else { + *dest += written; + } +} + +EXPORT unsigned long +MsgPrint(char *buffer, int bufSize, const char *format, unsigned long timeIn, const MsgDateFormat *dateInfo) +{ + const char *src; + char *dest; + char *limit; struct tm tms; + time_t utc; unsigned long i; - rataDie = 0; src = format; dest = buffer; limit = buffer + bufSize; - gmtime_r((time_t *)&timeIn, &tms); + utc = (time_t)timeIn; + gmtime_r(&utc, &tms); while (*src && dest < limit) { switch (*src) { @@ -531,13 +560,13 @@ MsgPrint(unsigned char *buffer, int bufSize, unsigned char *format, unsigned lon } case 'd': { - dest += snprintf(dest, limit - dest, "%02d", tms.tm_mday); + MsgAppendFormat(&dest, limit, "%02d", tms.tm_mday); src++; continue; } case 'H': { - dest += snprintf(dest, limit - dest, "%02d", tms.tm_hour); + MsgAppendFormat(&dest, limit, "%02d", tms.tm_hour); src++; continue; } @@ -545,9 +574,9 @@ MsgPrint(unsigned char *buffer, int bufSize, unsigned char *format, unsigned lon case 'I': { if (tms.tm_hour!=0) { if (tms.tm_hour>12) { - dest += snprintf(dest, limit - dest, "%02d", tms.tm_hour - 12); + MsgAppendFormat(&dest, limit, "%02d", tms.tm_hour - 12); } else { - dest += snprintf(dest, limit - dest, "%02d", tms.tm_hour); + MsgAppendFormat(&dest, limit, "%02d", tms.tm_hour); } } else { i = limit - dest > 2 ? 2 : limit - dest; @@ -559,19 +588,19 @@ MsgPrint(unsigned char *buffer, int bufSize, unsigned char *format, unsigned lon } case 'j': { - dest += snprintf(dest, limit - dest, "%d", tms.tm_yday + 1); + MsgAppendFormat(&dest, limit, "%d", tms.tm_yday + 1); src++; continue; } case 'm': { - dest += snprintf (dest, limit - dest, "%02d", tms.tm_mon + 1); + MsgAppendFormat(&dest, limit, "%02d", tms.tm_mon + 1); src++; continue; } case 'M': { - dest += snprintf(dest, limit - dest, "%02d", tms.tm_min); + MsgAppendFormat(&dest, limit, "%02d", tms.tm_min); src++; continue; } @@ -583,7 +612,7 @@ MsgPrint(unsigned char *buffer, int bufSize, unsigned char *format, unsigned lon } case 'p': { - dest += snprintf(dest, limit - dest, "%s", tms.tm_hour > 11 ? dateInfo->AmPm[1] : dateInfo->AmPm[0]); + MsgAppendFormat(&dest, limit, "%s", tms.tm_hour > 11 ? dateInfo->AmPm[1] : dateInfo->AmPm[0]); src++; continue; } @@ -591,19 +620,19 @@ MsgPrint(unsigned char *buffer, int bufSize, unsigned char *format, unsigned lon case 'r': { if (tms.tm_hour!=0) { if (tms.tm_hour>12) { - dest += snprintf(dest, limit - dest, "%02d:%02d:%02d %s", tms.tm_hour-12, tms.tm_min, tms.tm_sec, dateInfo->AmPm[1]); + MsgAppendFormat(&dest, limit, "%02d:%02d:%02d %s", tms.tm_hour-12, tms.tm_min, tms.tm_sec, dateInfo->AmPm[1]); } else { - dest += snprintf(dest, limit - dest, "%02d:%02d:%02d %s", tms.tm_hour, tms.tm_min, tms.tm_sec, tms.tm_hour > 11 ? dateInfo->AmPm[1] : dateInfo->AmPm[0]); + MsgAppendFormat(&dest, limit, "%02d:%02d:%02d %s", tms.tm_hour, tms.tm_min, tms.tm_sec, tms.tm_hour > 11 ? dateInfo->AmPm[1] : dateInfo->AmPm[0]); } } else { - dest += snprintf(dest, limit - dest, "12:%02d:%02d %s", tms.tm_min, tms.tm_sec, dateInfo->AmPm[0]); + MsgAppendFormat(&dest, limit, "12:%02d:%02d %s", tms.tm_min, tms.tm_sec, dateInfo->AmPm[0]); } src++; continue; } case 'S': { - dest += snprintf(dest, limit - dest, "%02d", tms.tm_sec); + MsgAppendFormat(&dest, limit, "%02d", tms.tm_sec); src++; continue; } @@ -615,7 +644,7 @@ MsgPrint(unsigned char *buffer, int bufSize, unsigned char *format, unsigned lon } case 'T': { - dest += snprintf(dest, limit - dest, "%02d:%02d:%02d", tms.tm_hour, tms.tm_min, tms.tm_sec); + MsgAppendFormat(&dest, limit, "%02d:%02d:%02d", tms.tm_hour, tms.tm_min, tms.tm_sec); src++; continue; } @@ -628,34 +657,34 @@ MsgPrint(unsigned char *buffer, int bufSize, unsigned char *format, unsigned lon rataDieToday = MsgGetRataDie(tms.tm_mday, tms.tm_mon + 1, tms.tm_year + 1900); i = rataDieYearStart % 7; if (i <= dateInfo->wDayStart) { - rataDieYearStart -= abs(dateInfo->wDayStart - 7); + rataDieYearStart -= 7 - dateInfo->wDayStart; } rataDieYearStart -= i; - dest += snprintf(dest, limit - dest, "%d", (int)((rataDieToday-rataDieYearStart) / 7 + 1)); + MsgAppendFormat(&dest, limit, "%d", (int)((rataDieToday-rataDieYearStart) / 7 + 1)); src++; continue; } case 'w': { - dest += snprintf(dest, limit - dest, "%d", tms.tm_wday); + MsgAppendFormat(&dest, limit, "%d", tms.tm_wday); src++; continue; } case 'y': { - dest += snprintf(dest, limit - dest, "%02d", tms.tm_year > 99 ? tms.tm_year - 100 : tms.tm_year); + MsgAppendFormat(&dest, limit, "%02d", tms.tm_year > 99 ? tms.tm_year - 100 : tms.tm_year); src++; continue; } case 'Y': { - dest += snprintf(dest, limit - dest, "%04d", tms.tm_year + 1900); + MsgAppendFormat(&dest, limit, "%04d", tms.tm_year + 1900); src++; continue; } case 'Z': { - dest += snprintf(dest, limit - dest, "%+4.4ld", dateInfo->timezoneOffset/36); + MsgAppendFormat(&dest, limit, "%+4.4ld", dateInfo->timezoneOffset/36); src++; continue; } @@ -984,8 +1013,9 @@ MsgGetUTCOffsetByUTC(unsigned long timezone, unsigned long utc) unsigned long year; unsigned long hour; struct tm tms; + time_t utcTime = (time_t)utc; - gmtime_r(&utc, &tms); + gmtime_r(&utcTime, &tms); year = tms.tm_year+1900; month = tms.tm_mon+1; day = tms.tm_mday; diff --git a/src/libs/msgapi/sqldb.c b/src/libs/msgapi/sqldb.c index 3ce5e92..ef5b623 100644 --- a/src/libs/msgapi/sqldb.c +++ b/src/libs/msgapi/sqldb.c @@ -31,11 +31,20 @@ MsgSQLOpen(char *path, BongoMemStack *memstack, int locktimeoutms) create = access(path, 0); - if (!(handle = MemMalloc(sizeof(MsgSQLHandle))) || - memset(handle, 0, sizeof(MsgSQLHandle)), 0 || - SQLITE_OK != sqlite3_open(path, &handle->db)) { + handle = MemMalloc(sizeof(MsgSQLHandle)); + if (!handle) { + Log(LOG_ERROR, "sql3: Out of memory opening database \"%s\"", path); + return NULL; + } + memset(handle, 0, sizeof(MsgSQLHandle)); + + if (SQLITE_OK != sqlite3_open(path, &handle->db)) { Log(LOG_ERROR, "sql3: Failed to open database \"%s\"", path); - goto fail; + if (handle->db) { + sqlite3_close(handle->db); + } + MemFree(handle); + return NULL; } Log(LOG_TRACE, "open handle %ld", handle->db); @@ -53,11 +62,6 @@ MsgSQLOpen(char *path, BongoMemStack *memstack, int locktimeoutms) return handle; -fail: - if (handle) { - MsgSQLClose(handle); - } - return NULL; } void @@ -135,7 +139,7 @@ int MsgSQLBeginTransaction(MsgSQLHandle *handle) { MsgSQLStatement *stmt; - int result, reset; + int result; int count = 1 + handle->lockTimeoutMs / MSGSQL_STMT_SLEEP_MS; BOOL locked = FALSE; @@ -154,7 +158,7 @@ MsgSQLBeginTransaction(MsgSQLHandle *handle) do { result = sqlite3_step(stmt->stmt); Log(LOG_TRACE, "sql3: BEGIN TRAN stmt %ld", stmt->stmt); - reset = sqlite3_reset(stmt->stmt); + sqlite3_reset(stmt->stmt); Log(LOG_TRACE, "sql: - bt reset stmt %ld", stmt->stmt); } while (SQLITE_BUSY == result && --count && (XplDelay(MSGSQL_STMT_SLEEP_MS), 1)); @@ -423,7 +427,7 @@ MsgSQLResultInt64(MsgSQLStatement *_stmt, int column) int MsgSQLResultText(MsgSQLStatement *_stmt, int column, char *result, size_t result_size) { - const char *out = sqlite3_column_text(_stmt->stmt, column); + const char *out = (const char *)sqlite3_column_text(_stmt->stmt, column); Log(LOG_TRACE, "sql3: - result text %ld", _stmt->stmt); if (out != NULL) strncpy(result, out, result_size); @@ -436,7 +440,7 @@ MsgSQLResultTextPtr(MsgSQLStatement *_stmt, int column, char **ptr) { *ptr = NULL; - const char *result = sqlite3_column_text(_stmt->stmt, column); + const char *result = (const char *)sqlite3_column_text(_stmt->stmt, column); if (result != NULL) *ptr = g_strdup(result); return 0; diff --git a/src/libs/python/bongo/CMakeLists.txt b/src/libs/python/bongo/CMakeLists.txt index 6ae24af..8e52a0f 100644 --- a/src/libs/python/bongo/CMakeLists.txt +++ b/src/libs/python/bongo/CMakeLists.txt @@ -5,7 +5,9 @@ configure_file(${CMAKE_CURRENT_SOURCE_DIR}/Xpl.py.in ${CMAKE_CURRENT_BINARY_DIR} install(DIRECTORY ./ DESTINATION "${PYTHON_SITEPACKAGES_PATH}/bongo/" - FILES_MATCHING PATTERN "*.py" + FILES_MATCHING + PATTERN "__pycache__" EXCLUDE + PATTERN "*.py" PATTERN ".svn" EXCLUDE ) diff --git a/src/libs/python/bongo/Contact.py b/src/libs/python/bongo/Contact.py index 9abb594..d6b2e7a 100644 --- a/src/libs/python/bongo/Contact.py +++ b/src/libs/python/bongo/Contact.py @@ -1,4 +1,4 @@ -import bongo.external.simplejson as simplejson +import json as simplejson class Contact: collapsed = ["fn", "note"] @@ -46,4 +46,3 @@ class Contact: def AsVcard(self): pass - diff --git a/src/libs/python/bongo/admin/ManagedSlapd.py b/src/libs/python/bongo/admin/ManagedSlapd.py index be121c8..dbea8da 100644 --- a/src/libs/python/bongo/admin/ManagedSlapd.py +++ b/src/libs/python/bongo/admin/ManagedSlapd.py @@ -6,6 +6,7 @@ import base64 import logging import os import pwd +import subprocess import random import hashlib import signal @@ -168,13 +169,27 @@ surname: Admin cn: admin userpassword: %(userpw)s """ % {"suffix" : self.suffix, "rootdn" : self.rootdn, - "userpw" : self.encodePassword("bongo")}) + "userpw" : self.encodePassword("bongo")}.encode("utf-8")) os.close(fd) - os.system("ldapadd -h 127.0.0.1 -p %d -x -D %s -w %s -f %s" % - (self.port, self.rootdn, self.password, tmpname)) - os.unlink(tmpname) + pwfd, pwname = tempfile.mkstemp(".password") + try: + os.fchmod(pwfd, 0o600) + os.write(pwfd, self.password.encode("utf-8")) + os.close(pwfd) + pwfd = None + subprocess.check_call([ + "ldapadd", "-h", "127.0.0.1", "-p", str(self.port), + "-x", "-D", self.rootdn, "-y", pwname, "-f", tmpname + ]) + finally: + if pwfd is not None: + os.close(pwfd) + if os.path.exists(pwname): + os.unlink(pwname) + if os.path.exists(tmpname): + os.unlink(tmpname) def writeSlapdConf(self, file, writeRootpw=True): ldapdir = "%s/ldap" % Xpl.DEFAULT_STATE_DIR diff --git a/src/libs/python/bongo/cmdparse.py b/src/libs/python/bongo/cmdparse.py index 1f9c636..b628696 100644 --- a/src/libs/python/bongo/cmdparse.py +++ b/src/libs/python/bongo/cmdparse.py @@ -162,7 +162,7 @@ class CommandParser(OptionParser): for group in groups: result.append("\n%s commands:\n" % group) commands = self.groups[group] - commands.sort(lambda x, y: cmp(x.get_name(), y.get_name())) + commands.sort(key=lambda command: command.get_name()) for cmd in commands: result.append(" %s%s %s\n" % (cmd.helpstr, " "*(max_cmd_length-len(cmd.helpstr)), cmd.summary)) diff --git a/src/libs/python/bongo/external/dateutil/__init__.py b/src/libs/python/bongo/external/dateutil/__init__.py deleted file mode 100644 index 84d7c3e..0000000 --- a/src/libs/python/bongo/external/dateutil/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -""" -Copyright (c) 2003-2005 Gustavo Niemeyer - -This module offers extensions to the standard python 2.3+ -datetime module. -""" -__author__ = "Gustavo Niemeyer " -__license__ = "PSF License" diff --git a/src/libs/python/bongo/external/dateutil/easter.py b/src/libs/python/bongo/external/dateutil/easter.py deleted file mode 100644 index 6a573e1..0000000 --- a/src/libs/python/bongo/external/dateutil/easter.py +++ /dev/null @@ -1,92 +0,0 @@ -""" -Copyright (c) 2003-2005 Gustavo Niemeyer - -This module offers extensions to the standard python 2.3+ -datetime module. -""" -__author__ = "Gustavo Niemeyer " -__license__ = "PSF License" - -import datetime - -__all__ = ["easter", "EASTER_JULIAN", "EASTER_ORTHODOX", "EASTER_WESTERN"] - -EASTER_JULIAN = 1 -EASTER_ORTHODOX = 2 -EASTER_WESTERN = 3 - -def easter(year, method=EASTER_WESTERN): - """ - This method was ported from the work done by GM Arts, - on top of the algorithm by Claus Tondering, which was - based in part on the algorithm of Ouding (1940), as - quoted in "Explanatory Supplement to the Astronomical - Almanac", P. Kenneth Seidelmann, editor. - - This algorithm implements three different easter - calculation methods: - - 1 - Original calculation in Julian calendar, valid in - dates after 326 AD - 2 - Original method, with date converted to Gregorian - calendar, valid in years 1583 to 4099 - 3 - Revised method, in Gregorian calendar, valid in - years 1583 to 4099 as well - - These methods are represented by the constants: - - EASTER_JULIAN = 1 - EASTER_ORTHODOX = 2 - EASTER_WESTERN = 3 - - The default method is method 3. - - More about the algorithm may be found at: - - http://users.chariot.net.au/~gmarts/eastalg.htm - - and - - http://www.tondering.dk/claus/calendar.html - - """ - - if not (1 <= method <= 3): - raise ValueError("invalid method") - - # g - Golden year - 1 - # c - Century - # h - (23 - Epact) mod 30 - # i - Number of days from March 21 to Paschal Full Moon - # j - Weekday for PFM (0=Sunday, etc) - # p - Number of days from March 21 to Sunday on or before PFM - # (-6 to 28 methods 1 & 3, to 56 for method 2) - # e - Extra days to add for method 2 (converting Julian - # date to Gregorian date) - - y = year - g = y % 19 - e = 0 - if method < 3: - # Old method - i = (19*g+15)%30 - j = (y+y/4+i)%7 - if method == 2: - # Extra dates to convert Julian to Gregorian date - e = 10 - if y > 1600: - e = e+y/100-16-(y/100-16)/4 - else: - # New method - c = y/100 - h = (c-c/4-(8*c+13)/25+19*g+15)%30 - i = h-(h/28)*(1-(h/28)*(29/(h+1))*((21-g)/11)) - j = (y+y/4+i+2-c+c/4)%7 - - # p can be from -6 to 56 corresponding to dates 22 March to 23 May - # (later dates apply to method 2, although 23 May never actually occurs) - p = i-j+e - d = 1+(p+27+(p+6)/40)%31 - m = 3+(p+26)/30 - return datetime.date(y,m,d) - diff --git a/src/libs/python/bongo/external/dateutil/parser.py b/src/libs/python/bongo/external/dateutil/parser.py deleted file mode 100644 index d5eda44..0000000 --- a/src/libs/python/bongo/external/dateutil/parser.py +++ /dev/null @@ -1,875 +0,0 @@ -# -*- coding:iso-8859-1 -*- -""" -Copyright (c) 2003-2005 Gustavo Niemeyer - -This module offers extensions to the standard python 2.3+ -datetime module. -""" -__author__ = "Gustavo Niemeyer " -__license__ = "PSF License" - -import os.path -import string -import sys -import time - -import datetime -from . import relativedelta -from . import tz - -__all__ = ["parse", "parserinfo"] - -# Some pointers: -# -# http://www.cl.cam.ac.uk/~mgk25/iso-time.html -# http://www.iso.ch/iso/en/prods-services/popstds/datesandtime.html -# http://www.w3.org/TR/NOTE-datetime -# http://ringmaster.arc.nasa.gov/tools/time_formats.html -# http://search.cpan.org/author/MUIR/Time-modules-2003.0211/lib/Time/ParseDate.pm -# http://stein.cshl.org/jade/distrib/docs/java.text.SimpleDateFormat.html - -try: - from io import StringIO -except ImportError: - from io import StringIO - -class _timelex: - def __init__(self, instream): - if isinstance(instream, str): - instream = StringIO(instream) - self.instream = instream - self.wordchars = ('abcdfeghijklmnopqrstuvwxyz' - 'ABCDEFGHIJKLMNOPQRSTUVWXYZ_' - 'ßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ' - 'ÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞ') - self.numchars = '0123456789' - self.whitespace = ' \t\r\n' - self.charstack = [] - self.tokenstack = [] - self.eof = False - - def get_token(self): - if self.tokenstack: - return self.tokenstack.pop(0) - seenletters = False - token = None - state = None - wordchars = self.wordchars - numchars = self.numchars - whitespace = self.whitespace - while not self.eof: - if self.charstack: - nextchar = self.charstack.pop(0) - else: - nextchar = self.instream.read(1) - while nextchar == '\x00': - nextchar = self.instream.read(1) - if not nextchar: - self.eof = True - break - elif not state: - token = nextchar - if nextchar in wordchars: - state = 'a' - elif nextchar in numchars: - state = '0' - elif nextchar in whitespace: - token = ' ' - break # emit token - else: - break # emit token - elif state == 'a': - seenletters = True - if nextchar in wordchars: - token += nextchar - elif nextchar == '.': - token += nextchar - state = 'a.' - else: - self.charstack.append(nextchar) - break # emit token - elif state == '0': - if nextchar in numchars: - token += nextchar - elif nextchar == '.': - token += nextchar - state = '0.' - else: - self.charstack.append(nextchar) - break # emit token - elif state == 'a.': - seenletters = True - if nextchar == '.' or nextchar in wordchars: - token += nextchar - elif nextchar in numchars and token[-1] == '.': - token += nextchar - state = '0.' - else: - self.charstack.append(nextchar) - break # emit token - elif state == '0.': - if nextchar == '.' or nextchar in numchars: - token += nextchar - elif nextchar in wordchars and token[-1] == '.': - token += nextchar - state = 'a.' - else: - self.charstack.append(nextchar) - break # emit token - if (state in ('a.', '0.') and - (seenletters or token.count('.') > 1 or token[-1] == '.')): - l = token.split('.') - token = l[0] - for tok in l[1:]: - self.tokenstack.append('.') - if tok: - self.tokenstack.append(tok) - return token - - def __iter__(self): - return self - - def __next__(self): - token = self.get_token() - if token is None: - raise StopIteration - return token - - def split(cls, s): - return list(cls(s)) - split = classmethod(split) - -class _resultbase(object): - - def __init__(self): - for attr in self.__slots__: - setattr(self, attr, None) - - def _repr(self, classname): - l = [] - for attr in self.__slots__: - value = getattr(self, attr) - if value is not None: - l.append("%s=%s" % (attr, repr(value))) - return "%s(%s)" % (classname, ", ".join(l)) - - def __repr__(self): - return self._repr(self.__class__.__name__) - -class parserinfo: - - # m from a.m/p.m, t from ISO T separator - JUMP = [" ", ".", ",", ";", "-", "/", "'", - "at", "on", "and", "ad", "m", "t", "of", - "st", "nd", "rd", "th"] - - WEEKDAYS = [("Mon", "Monday"), - ("Tue", "Tuesday"), - ("Wed", "Wednesday"), - ("Thu", "Thursday"), - ("Fri", "Friday"), - ("Sat", "Saturday"), - ("Sun", "Sunday")] - MONTHS = [("Jan", "January"), - ("Feb", "February"), - ("Mar", "March"), - ("Apr", "April"), - ("May", "May"), - ("Jun", "June"), - ("Jul", "July"), - ("Aug", "August"), - ("Sep", "September"), - ("Oct", "October"), - ("Nov", "November"), - ("Dec", "December")] - HMS = [("h", "hour", "hours"), - ("m", "minute", "minutes"), - ("s", "second", "seconds")] - AMPM = [("am", "a"), - ("pm", "p")] - UTCZONE = ["UTC", "GMT", "Z"] - PERTAIN = ["of"] - TZOFFSET = {} - - def __init__(self, dayfirst=False, yearfirst=False): - self._jump = self._convert(self.JUMP) - self._weekdays = self._convert(self.WEEKDAYS) - self._months = self._convert(self.MONTHS) - self._hms = self._convert(self.HMS) - self._ampm = self._convert(self.AMPM) - self._utczone = self._convert(self.UTCZONE) - self._pertain = self._convert(self.PERTAIN) - - self.dayfirst = dayfirst - self.yearfirst = yearfirst - - self._year = time.localtime().tm_year - self._century = self._year/100*100 - - def _convert(self, lst): - dct = {} - for i in range(len(lst)): - v = lst[i] - if isinstance(v, tuple): - for v in v: - dct[v.lower()] = i - else: - dct[v.lower()] = i - return dct - - def jump(self, name): - return name.lower() in self._jump - - def weekday(self, name): - if len(name) >= 3: - try: - return self._weekdays[name.lower()] - except KeyError: - pass - return None - - def month(self, name): - if len(name) >= 3: - try: - return self._months[name.lower()]+1 - except KeyError: - pass - return None - - def hms(self, name): - try: - return self._hms[name.lower()] - except KeyError: - return None - - def ampm(self, name): - try: - return self._ampm[name.lower()] - except KeyError: - return None - - def pertain(self, name): - return name.lower() in self._pertain - - def utczone(self, name): - return name.lower() in self._utczone - - def tzoffset(self, name): - if name in self._utczone: - return 0 - return self.TZOFFSET.get(name) - - def convertyear(self, year): - if year < 100: - year += self._century - if abs(year-self._year) >= 50: - if year < self._year: - year += 100 - else: - year -= 100 - return year - - def validate(self, res): - # move to info - if res.year is not None: - res.year = self.convertyear(res.year) - if res.tzoffset == 0 and not res.tzname or res.tzname == 'Z': - res.tzname = "UTC" - res.tzoffset = 0 - elif res.tzoffset != 0 and res.tzname and self.utczone(res.tzname): - res.tzoffset = 0 - return True - - -class parser: - - def __init__(self, info=parserinfo): - if issubclass(info, parserinfo): - self.info = parserinfo() - elif isinstance(info, parserinfo): - self.info = info - else: - raise TypeError("Unsupported parserinfo type") - - def parse(self, timestr, default=None, - ignoretz=False, tzinfos=None, - **kwargs): - if not default: - default = datetime.datetime.now().replace(hour=0, minute=0, - second=0, microsecond=0) - res = self._parse(timestr, **kwargs) - if res is None: - raise ValueError("unknown string format") - repl = {} - for attr in ["year", "month", "day", "hour", - "minute", "second", "microsecond"]: - value = getattr(res, attr) - if value is not None: - repl[attr] = value - ret = default.replace(**repl) - if res.weekday is not None and not res.day: - ret = ret+relativedelta.relativedelta(weekday=res.weekday) - if not ignoretz: - if callable(tzinfos) or tzinfos and res.tzname in tzinfos: - if callable(tzinfos): - tzdata = tzinfos(res.tzname, res.tzoffset) - else: - tzdata = tzinfos.get(res.tzname) - if isinstance(tzdata, datetime.tzinfo): - tzinfo = tzdata - elif isinstance(tzdata, str): - tzinfo = tz.tzstr(tzdata) - elif isinstance(tzdata, int): - tzinfo = tz.tzoffset(res.tzname, tzdata) - else: - raise ValueError("offset must be tzinfo subclass, " \ - "tz string, or int offset") - ret = ret.replace(tzinfo=tzinfo) - elif res.tzname and res.tzname in time.tzname: - ret = ret.replace(tzinfo=tz.tzlocal()) - elif res.tzoffset == 0: - ret = ret.replace(tzinfo=tz.tzutc()) - elif res.tzoffset: - ret = ret.replace(tzinfo=tz.tzoffset(res.tzname, res.tzoffset)) - return ret - - class _result(_resultbase): - __slots__ = ["year", "month", "day", "weekday", - "hour", "minute", "second", "microsecond", - "tzname", "tzoffset"] - - def _parse(self, timestr, dayfirst=None, yearfirst=None, fuzzy=False): - info = self.info - if dayfirst is None: - dayfirst = info.dayfirst - if yearfirst is None: - yearfirst = info.yearfirst - res = self._result() - l = _timelex.split(timestr) - try: - - # year/month/day list - ymd = [] - - # Index of the month string in ymd - mstridx = -1 - - len_l = len(l) - i = 0 - while i < len_l: - - # Check if it's a number - try: - value = float(l[i]) - except ValueError: - value = None - if value is not None: - # Token is a number - len_li = len(l[i]) - i += 1 - if (len(ymd) == 3 and len_li in (2, 4) - and (i >= len_l or (l[i] != ':' and - info.hms(l[i]) is None))): - # 19990101T23[59] - s = l[i-1] - res.hour = int(s[:2]) - if len_li == 4: - res.minute = int(s[2:]) - elif len_li == 6 or (len_li > 6 and l[i-1].find('.') == 6): - # YYMMDD or HHMMSS[.ss] - s = l[i-1] - if not ymd and l[i-1].find('.') == -1: - ymd.append(info.convertyear(int(s[:2]))) - ymd.append(int(s[2:4])) - ymd.append(int(s[4:])) - else: - # 19990101T235959[.59] - res.hour = int(s[:2]) - res.minute = int(s[2:4]) - value = float(s[4:]) - res.second = int(value) - if value%1: - res.microsecond = int(1000000*(value%1)) - elif len_li == 8: - # YYYYMMDD - s = l[i-1] - ymd.append(int(s[:4])) - ymd.append(int(s[4:6])) - ymd.append(int(s[6:])) - elif len_li in (12, 14): - # YYYYMMDDhhmm[ss] - s = l[i-1] - ymd.append(int(s[:4])) - ymd.append(int(s[4:6])) - ymd.append(int(s[6:8])) - res.hour = int(s[8:10]) - res.minute = int(s[10:12]) - if len_li == 14: - res.second = int(s[12:]) - elif ((i < len_l and info.hms(l[i]) is not None) or - (i+1 < len_l and l[i] == ' ' and - info.hms(l[i+1]) is not None)): - # HH[ ]h or MM[ ]m or SS[.ss][ ]s - if l[i] == ' ': - i += 1 - idx = info.hms(l[i]) - while True: - if idx == 0: - res.hour = int(value) - if value%1: - res.minute = int(60*(value%1)) - elif idx == 1: - res.minute = int(value) - if value%1: - res.second = int(60*(value%1)) - elif idx == 2: - res.second = int(value) - if value%1: - res.microsecond = int(1000000*(value%1)) - i += 1 - if i >= len_l or idx == 2: - break - # 12h00 - try: - value = float(l[i]) - except ValueError: - break - else: - i += 1 - idx += 1 - if i < len_l: - newidx = info.hms(l[i]) - if newidx is not None: - idx = newidx - elif i+1 < len_l and l[i] == ':': - # HH:MM[:SS[.ss]] - res.hour = int(value) - i += 1 - value = float(l[i]) - res.minute = int(value) - if value%1: - res.second = int(60*(value%1)) - i += 1 - if i < len_l and l[i] == ':': - value = float(l[i+1]) - res.second = int(value) - if value%1: - res.microsecond = int(1000000*(value%1)) - i += 2 - elif i < len_l and l[i] in ('-', '/', '.'): - sep = l[i] - ymd.append(int(value)) - i += 1 - if i < len_l and not info.jump(l[i]): - try: - # 01-01[-01] - ymd.append(int(l[i])) - except ValueError: - # 01-Jan[-01] - value = info.month(l[i]) - if value is not None: - ymd.append(value) - assert mstridx == -1 - mstridx = len(ymd)-1 - else: - return None - i += 1 - if i < len_l and l[i] == sep: - # We have three members - i += 1 - value = info.month(l[i]) - if value is not None: - ymd.append(value) - mstridx = len(ymd)-1 - assert mstridx == -1 - else: - ymd.append(int(l[i])) - i += 1 - elif i >= len_l or info.jump(l[i]): - if i+1 < len_l and info.ampm(l[i+1]) is not None: - # 12 am - res.hour = int(value) - if res.hour < 12 and info.ampm(l[i+1]) == 1: - res.hour += 12 - elif res.hour == 12 and info.ampm(l[i+1]) == 0: - res.hour = 0 - i += 1 - else: - # Year, month or day - ymd.append(int(value)) - i += 1 - elif info.ampm(l[i]) is not None: - # 12am - res.hour = int(value) - if res.hour < 12 and info.ampm(l[i]) == 1: - res.hour += 12 - elif res.hour == 12 and info.ampm(l[i]) == 0: - res.hour = 0 - i += 1 - elif not fuzzy: - return None - else: - i += 1 - continue - - # Check weekday - value = info.weekday(l[i]) - if value is not None: - res.weekday = value - i += 1 - continue - - # Check month name - value = info.month(l[i]) - if value is not None: - ymd.append(value) - assert mstridx == -1 - mstridx = len(ymd)-1 - i += 1 - if i < len_l: - if l[i] in ('-', '/'): - # Jan-01[-99] - sep = l[i] - i += 1 - ymd.append(int(l[i])) - i += 1 - if i < len_l and l[i] == sep: - # Jan-01-99 - i += 1 - ymd.append(int(l[i])) - i += 1 - elif (i+3 < len_l and l[i] == l[i+2] == ' ' - and info.pertain(l[i+1])): - # Jan of 01 - # In this case, 01 is clearly year - try: - value = int(l[i+3]) - except ValueError: - # Wrong guess - pass - else: - # Convert it here to become unambiguous - ymd.append(info.convertyear(value)) - i += 4 - continue - - # Check am/pm - value = info.ampm(l[i]) - if value is not None: - if value == 1 and res.hour < 12: - res.hour += 12 - elif value == 0 and res.hour == 12: - res.hour = 0 - i += 1 - continue - - # Check for a timezone name - if (res.hour is not None and len(l[i]) <= 5 and - res.tzname is None and res.tzoffset is None and - not [x for x in l[i] if x not in string.ascii_uppercase]): - res.tzname = l[i] - res.tzoffset = info.tzoffset(res.tzname) - i += 1 - - # Check for something like GMT+3, or BRST+3. Notice - # that it doesn't mean "I am 3 hours after GMT", but - # "my time +3 is GMT". If found, we reverse the - # logic so that timezone parsing code will get it - # right. - if i < len_l and l[i] in ('+', '-'): - l[i] = ('+', '-')[l[i] == '+'] - res.tzoffset = None - if info.utczone(res.tzname): - # With something like GMT+3, the timezone - # is *not* GMT. - res.tzname = None - - continue - - # Check for a numbered timezone - if res.hour is not None and l[i] in ('+', '-'): - signal = (-1,1)[l[i] == '+'] - i += 1 - len_li = len(l[i]) - if len_li == 4: - # -0300 - res.tzoffset = int(l[i][:2])*3600+int(l[i][2:])*60 - elif i+1 < len_l and l[i+1] == ':': - # -03:00 - res.tzoffset = int(l[i])*3600+int(l[i+2])*60 - i += 2 - elif len_li <= 2: - # -[0]3 - res.tzoffset = int(l[i][:2])*3600 - else: - return None - i += 1 - res.tzoffset *= signal - - # Look for a timezone name between parenthesis - if (i+3 < len_l and - info.jump(l[i]) and l[i+1] == '(' and l[i+3] == ')' and - 3 <= len(l[i+2]) <= 5 and - not [x for x in l[i+2] - if x not in string.ascii_uppercase]): - # -0300 (BRST) - res.tzname = l[i+2] - i += 4 - continue - - # Check jumps - if not (info.jump(l[i]) or fuzzy): - return None - - i += 1 - - # Process year/month/day - len_ymd = len(ymd) - if len_ymd > 3: - # More than three members!? - return None - elif len_ymd == 1 or (mstridx != -1 and len_ymd == 2): - # One member, or two members with a month string - if mstridx != -1: - res.month = ymd[mstridx] - del ymd[mstridx] - if len_ymd > 1 or mstridx == -1: - if ymd[0] > 31: - res.year = ymd[0] - else: - res.day = ymd[0] - elif len_ymd == 2: - # Two members with numbers - if ymd[0] > 31: - # 99-01 - res.year, res.month = ymd - elif ymd[1] > 31: - # 01-99 - res.month, res.year = ymd - elif dayfirst and ymd[1] <= 12: - # 13-01 - res.day, res.month = ymd - else: - # 01-13 - res.month, res.day = ymd - if len_ymd == 3: - # Three members - if mstridx == 0: - res.month, res.day, res.year = ymd - elif mstridx == 1: - if ymd[0] > 31 or (yearfirst and ymd[2] <= 31): - # 99-Jan-01 - res.year, res.month, res.day = ymd - else: - # 01-Jan-01 - # Give precendence to day-first, since - # two-digit years is usually hand-written. - res.day, res.month, res.year = ymd - elif mstridx == 2: - # WTF!? - if ymd[1] > 31: - # 01-99-Jan - res.day, res.year, res.month = ymd - else: - # 99-01-Jan - res.year, res.day, res.month = ymd - else: - if ymd[0] > 31 or \ - (yearfirst and ymd[1] <= 12 and ymd[2] <= 31): - # 99-01-01 - res.year, res.month, res.day = ymd - elif ymd[0] > 12 or (dayfirst and ymd[1] <= 12): - # 13-01-01 - res.day, res.month, res.year = ymd - else: - # 01-13-01 - res.month, res.day, res.year = ymd - - except (IndexError, ValueError, AssertionError): - return None - - if not info.validate(res): - return None - return res - -DEFAULTPARSER = parser() -def parse(timestr, parserinfo=None, **kwargs): - if parserinfo: - return parser(parserinfo).parse(timestr, **kwargs) - else: - return DEFAULTPARSER.parse(timestr, **kwargs) - -class _tzparser: - - class _result(_resultbase): - - __slots__ = ["stdabbr", "stdoffset", "dstabbr", "dstoffset", - "start", "end"] - - class _attr(_resultbase): - __slots__ = ["month", "week", "weekday", - "yday", "jyday", "day", "time"] - - def __repr__(self): - return self._repr("") - - def __init__(self): - _resultbase.__init__(self) - self.start = self._attr() - self.end = self._attr() - - def parse(self, tzstr): - res = self._result() - l = _timelex.split(tzstr) - try: - - len_l = len(l) - - i = 0 - while i < len_l: - # BRST+3[BRDT[+2]] - j = i - while j < len_l and not [x for x in l[j] - if x in "0123456789:,-+"]: - j += 1 - if j != i: - if not res.stdabbr: - offattr = "stdoffset" - res.stdabbr = "".join(l[i:j]) - else: - offattr = "dstoffset" - res.dstabbr = "".join(l[i:j]) - i = j - if (i < len_l and - (l[i] in ('+', '-') or l[i][0] in "0123456789")): - if l[i] in ('+', '-'): - signal = (1,-1)[l[i] == '+'] - i += 1 - else: - signal = -1 - len_li = len(l[i]) - if len_li == 4: - # -0300 - setattr(res, offattr, - (int(l[i][:2])*3600+int(l[i][2:])*60)*signal) - elif i+1 < len_l and l[i+1] == ':': - # -03:00 - setattr(res, offattr, - (int(l[i])*3600+int(l[i+2])*60)*signal) - i += 2 - elif len_li <= 2: - # -[0]3 - setattr(res, offattr, - int(l[i][:2])*3600*signal) - else: - return None - i += 1 - if res.dstabbr: - break - else: - break - - if i < len_l: - for j in range(i, len_l): - if l[j] == ';': l[j] = ',' - - assert l[i] == ',' - - i += 1 - - if i >= len_l: - pass - elif (8 <= l.count(',') <= 9 and - not [y for x in l[i:] if x != ',' - for y in x if y not in "0123456789"]): - # GMT0BST,3,0,30,3600,10,0,26,7200[,3600] - for x in (res.start, res.end): - x.month = int(l[i]) - i += 2 - if l[i] == '-': - value = int(l[i+1])*-1 - i += 1 - else: - value = int(l[i]) - i += 2 - if value: - x.week = value - x.weekday = (int(l[i])-1)%7 - else: - x.day = int(l[i]) - i += 2 - x.time = int(l[i]) - i += 2 - if i < len_l: - if l[i] in ('-','+'): - signal = (-1,1)[l[i] == "+"] - i += 1 - else: - signal = 1 - res.dstoffset = (res.stdoffset+int(l[i]))*signal - elif (l.count(',') == 2 and l[i:].count('/') <= 2 and - not [y for x in l[i:] if x not in (',','/','J','M', - '.','-',':') - for y in x if y not in "0123456789"]): - for x in (res.start, res.end): - if l[i] == 'J': - # non-leap year day (1 based) - i += 1 - x.jyday = int(l[i]) - elif l[i] == 'M': - # month[-.]week[-.]weekday - i += 1 - x.month = int(l[i]) - i += 1 - assert l[i] in ('-', '.') - i += 1 - x.week = int(l[i]) - if x.week == 5: - x.week = -1 - i += 1 - assert l[i] in ('-', '.') - i += 1 - x.weekday = (int(l[i])-1)%7 - else: - # year day (zero based) - x.yday = int(l[i])+1 - - i += 1 - - if i < len_l and l[i] == '/': - i += 1 - # start time - len_li = len(l[i]) - if len_li == 4: - # -0300 - x.time = (int(l[i][:2])*3600+int(l[i][2:])*60) - elif i+1 < len_l and l[i+1] == ':': - # -03:00 - x.time = int(l[i])*3600+int(l[i+2])*60 - i += 2 - if i+1 < len_l and l[i+1] == ':': - i += 2 - x.time += int(l[i]) - elif len_li <= 2: - # -[0]3 - x.time = (int(l[i][:2])*3600) - else: - return None - i += 1 - - assert i == len_l or l[i] == ',' - - i += 1 - - assert i >= len_l - - except (IndexError, ValueError, AssertionError): - return None - - return res - -DEFAULTTZPARSER = _tzparser() -def _parsetz(tzstr): - return DEFAULTTZPARSER.parse(tzstr) - -# vim:ts=4:sw=4:et diff --git a/src/libs/python/bongo/external/dateutil/relativedelta.py b/src/libs/python/bongo/external/dateutil/relativedelta.py deleted file mode 100644 index dc6c76e..0000000 --- a/src/libs/python/bongo/external/dateutil/relativedelta.py +++ /dev/null @@ -1,432 +0,0 @@ -""" -Copyright (c) 2003-2005 Gustavo Niemeyer - -This module offers extensions to the standard python 2.3+ -datetime module. -""" -__author__ = "Gustavo Niemeyer " -__license__ = "PSF License" - -import datetime -import calendar - -__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"] - -class weekday(object): - __slots__ = ["weekday", "n"] - - def __init__(self, weekday, n=None): - self.weekday = weekday - self.n = n - - def __call__(self, n): - if n == self.n: - return self - else: - return self.__class__(self.weekday, n) - - def __eq__(self, other): - try: - if self.weekday != other.weekday or self.n != other.n: - return False - except AttributeError: - return False - return True - - def __repr__(self): - s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday] - if not self.n: - return s - else: - return "%s(%+d)" % (s, self.n) - -MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)]) - -class relativedelta: - """ -The relativedelta type is based on the specification of the excelent -work done by M.-A. Lemburg in his mx.DateTime extension. However, -notice that this type does *NOT* implement the same algorithm as -his work. Do *NOT* expect it to behave like mx.DateTime's counterpart. - -There's two different ways to build a relativedelta instance. The -first one is passing it two date/datetime classes: - - relativedelta(datetime1, datetime2) - -And the other way is to use the following keyword arguments: - - year, month, day, hour, minute, second, microsecond: - Absolute information. - - years, months, weeks, days, hours, minutes, seconds, microseconds: - Relative information, may be negative. - - weekday: - One of the weekday instances (MO, TU, etc). These instances may - receive a parameter N, specifying the Nth weekday, which could - be positive or negative (like MO(+1) or MO(-2). Not specifying - it is the same as specifying +1. You can also use an integer, - where 0=MO. - - leapdays: - Will add given days to the date found, if year is a leap - year, and the date found is post 28 of february. - - yearday, nlyearday: - Set the yearday or the non-leap year day (jump leap days). - These are converted to day/month/leapdays information. - -Here is the behavior of operations with relativedelta: - -1) Calculate the absolute year, using the 'year' argument, or the - original datetime year, if the argument is not present. - -2) Add the relative 'years' argument to the absolute year. - -3) Do steps 1 and 2 for month/months. - -4) Calculate the absolute day, using the 'day' argument, or the - original datetime day, if the argument is not present. Then, - subtract from the day until it fits in the year and month - found after their operations. - -5) Add the relative 'days' argument to the absolute day. Notice - that the 'weeks' argument is multiplied by 7 and added to - 'days'. - -6) Do steps 1 and 2 for hour/hours, minute/minutes, second/seconds, - microsecond/microseconds. - -7) If the 'weekday' argument is present, calculate the weekday, - with the given (wday, nth) tuple. wday is the index of the - weekday (0-6, 0=Mon), and nth is the number of weeks to add - forward or backward, depending on its signal. Notice that if - the calculated date is already Monday, for example, using - (0, 1) or (0, -1) won't change the day. - """ - - def __init__(self, dt1=None, dt2=None, - years=0, months=0, days=0, leapdays=0, weeks=0, - hours=0, minutes=0, seconds=0, microseconds=0, - year=None, month=None, day=None, weekday=None, - yearday=None, nlyearday=None, - hour=None, minute=None, second=None, microsecond=None): - if dt1 and dt2: - if not isinstance(dt1, datetime.date) or \ - not isinstance(dt2, datetime.date): - raise TypeError("relativedelta only diffs datetime/date") - if type(dt1) is not type(dt2): - if not isinstance(dt1, datetime.datetime): - dt1 = datetime.datetime.fromordinal(dt1.toordinal()) - elif not isinstance(dt2, datetime.datetime): - dt2 = datetime.datetime.fromordinal(dt2.toordinal()) - self.years = 0 - self.months = 0 - self.days = 0 - self.leapdays = 0 - self.hours = 0 - self.minutes = 0 - self.seconds = 0 - self.microseconds = 0 - self.year = None - self.month = None - self.day = None - self.weekday = None - self.hour = None - self.minute = None - self.second = None - self.microsecond = None - self._has_time = 0 - - months = (dt1.year*12+dt1.month)-(dt2.year*12+dt2.month) - self._set_months(months) - dtm = self.__radd__(dt2) - if dt1 < dt2: - while dt1 > dtm: - months += 1 - self._set_months(months) - dtm = self.__radd__(dt2) - else: - while dt1 < dtm: - months -= 1 - self._set_months(months) - dtm = self.__radd__(dt2) - delta = dt1 - dtm - self.seconds = delta.seconds+delta.days*86400 - self.microseconds = delta.microseconds - else: - self.years = years - self.months = months - self.days = days+weeks*7 - self.leapdays = leapdays - self.hours = hours - self.minutes = minutes - self.seconds = seconds - self.microseconds = microseconds - self.year = year - self.month = month - self.day = day - self.hour = hour - self.minute = minute - self.second = second - self.microsecond = microsecond - - if type(weekday) is int: - self.weekday = weekdays[weekday] - else: - self.weekday = weekday - - yday = 0 - if nlyearday: - yday = nlyearday - elif yearday: - yday = yearday - if yearday > 59: - self.leapdays = -1 - if yday: - ydayidx = [31,59,90,120,151,181,212,243,273,304,334,366] - for idx, ydays in enumerate(ydayidx): - if yday <= ydays: - self.month = idx+1 - if idx == 0: - self.day = ydays - else: - self.day = yday-ydayidx[idx-1] - break - else: - raise ValueError("invalid year day (%d)" % yday) - - self._fix() - - def _fix(self): - if abs(self.microseconds) > 999999: - s = self.microseconds/abs(self.microseconds) - div, mod = divmod(self.microseconds*s, 1000000) - self.microseconds = mod*s - self.seconds += div*s - if abs(self.seconds) > 59: - s = self.seconds/abs(self.seconds) - div, mod = divmod(self.seconds*s, 60) - self.seconds = mod*s - self.minutes += div*s - if abs(self.minutes) > 59: - s = self.minutes/abs(self.minutes) - div, mod = divmod(self.minutes*s, 60) - self.minutes = mod*s - self.hours += div*s - if abs(self.hours) > 23: - s = self.hours/abs(self.hours) - div, mod = divmod(self.hours*s, 24) - self.hours = mod*s - self.days += div*s - if abs(self.months) > 11: - s = self.months/abs(self.months) - div, mod = divmod(self.months*s, 12) - self.months = mod*s - self.years += div*s - if (self.hours or self.minutes or self.seconds or self.microseconds or - self.hour is not None or self.minute is not None or - self.second is not None or self.microsecond is not None): - self._has_time = 1 - else: - self._has_time = 0 - - def _set_months(self, months): - self.months = months - if abs(self.months) > 11: - s = self.months/abs(self.months) - div, mod = divmod(self.months*s, 12) - self.months = mod*s - self.years = div*s - else: - self.years = 0 - - def __radd__(self, other): - if not isinstance(other, datetime.date): - raise TypeError("unsupported type for add operation") - elif self._has_time and not isinstance(other, datetime.datetime): - other = datetime.datetime.fromordinal(other.toordinal()) - year = (self.year or other.year)+self.years - month = self.month or other.month - if self.months: - assert 1 <= abs(self.months) <= 12 - month += self.months - if month > 12: - year += 1 - month -= 12 - elif month < 1: - year -= 1 - month += 12 - day = min(calendar.monthrange(year, month)[1], - self.day or other.day) - repl = {"year": year, "month": month, "day": day} - for attr in ["hour", "minute", "second", "microsecond"]: - value = getattr(self, attr) - if value is not None: - repl[attr] = value - days = self.days - if self.leapdays and month > 2 and calendar.isleap(year): - days += self.leapdays - ret = (other.replace(**repl) - + datetime.timedelta(days=days, - hours=self.hours, - minutes=self.minutes, - seconds=self.seconds, - microseconds=self.microseconds)) - if self.weekday: - weekday, nth = self.weekday.weekday, self.weekday.n or 1 - jumpdays = (abs(nth)-1)*7 - if nth > 0: - jumpdays += (7-ret.weekday()+weekday)%7 - else: - jumpdays += (ret.weekday()-weekday)%7 - jumpdays *= -1 - ret += datetime.timedelta(days=jumpdays) - return ret - - def __rsub__(self, other): - return self.__neg__().__radd__(other) - - def __add__(self, other): - if not isinstance(other, relativedelta): - raise TypeError("unsupported type for add operation") - return relativedelta(years=other.years+self.years, - months=other.months+self.months, - days=other.days+self.days, - hours=other.hours+self.hours, - minutes=other.minutes+self.minutes, - seconds=other.seconds+self.seconds, - microseconds=other.microseconds+self.microseconds, - leapdays=other.leapdays or self.leapdays, - year=other.year or self.year, - month=other.month or self.month, - day=other.day or self.day, - weekday=other.weekday or self.weekday, - hour=other.hour or self.hour, - minute=other.minute or self.minute, - second=other.second or self.second, - microsecond=other.second or self.microsecond) - - def __sub__(self, other): - if not isinstance(other, relativedelta): - raise TypeError("unsupported type for sub operation") - return relativedelta(years=other.years-self.years, - months=other.months-self.months, - days=other.days-self.days, - hours=other.hours-self.hours, - minutes=other.minutes-self.minutes, - seconds=other.seconds-self.seconds, - microseconds=other.microseconds-self.microseconds, - leapdays=other.leapdays or self.leapdays, - year=other.year or self.year, - month=other.month or self.month, - day=other.day or self.day, - weekday=other.weekday or self.weekday, - hour=other.hour or self.hour, - minute=other.minute or self.minute, - second=other.second or self.second, - microsecond=other.second or self.microsecond) - - def __neg__(self): - return relativedelta(years=-self.years, - months=-self.months, - days=-self.days, - hours=-self.hours, - minutes=-self.minutes, - seconds=-self.seconds, - microseconds=-self.microseconds, - leapdays=self.leapdays, - year=self.year, - month=self.month, - day=self.day, - weekday=self.weekday, - hour=self.hour, - minute=self.minute, - second=self.second, - microsecond=self.microsecond) - - def __bool__(self): - return not (not self.years and - not self.months and - not self.days and - not self.hours and - not self.minutes and - not self.seconds and - not self.microseconds and - not self.leapdays and - self.year is None and - self.month is None and - self.day is None and - self.weekday is None and - self.hour is None and - self.minute is None and - self.second is None and - self.microsecond is None) - - def __mul__(self, other): - f = float(other) - return relativedelta(years=self.years*f, - months=self.months*f, - days=self.days*f, - hours=self.hours*f, - minutes=self.minutes*f, - seconds=self.seconds*f, - microseconds=self.microseconds*f, - leapdays=self.leapdays, - year=self.year, - month=self.month, - day=self.day, - weekday=self.weekday, - hour=self.hour, - minute=self.minute, - second=self.second, - microsecond=self.microsecond) - - def __eq__(self, other): - if not isinstance(other, relativedelta): - return False - if self.weekday or other.weekday: - if not self.weekday or not other.weekday: - return False - if self.weekday.weekday != other.weekday.weekday: - return False - n1, n2 = self.weekday.n, other.weekday.n - if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)): - return False - return (self.years == other.years and - self.months == other.months and - self.days == other.days and - self.hours == other.hours and - self.minutes == other.minutes and - self.seconds == other.seconds and - self.leapdays == other.leapdays and - self.year == other.year and - self.month == other.month and - self.day == other.day and - self.hour == other.hour and - self.minute == other.minute and - self.second == other.second and - self.microsecond == other.microsecond) - - def __ne__(self, other): - return not self.__eq__(other) - - def __div__(self, other): - return self.__mul__(1/float(other)) - - def __repr__(self): - l = [] - for attr in ["years", "months", "days", "leapdays", - "hours", "minutes", "seconds", "microseconds"]: - value = getattr(self, attr) - if value: - l.append("%s=%+d" % (attr, value)) - for attr in ["year", "month", "day", "weekday", - "hour", "minute", "second", "microsecond"]: - value = getattr(self, attr) - if value is not None: - l.append("%s=%s" % (attr, repr(value))) - return "%s(%s)" % (self.__class__.__name__, ", ".join(l)) - -# vim:ts=4:sw=4:et diff --git a/src/libs/python/bongo/external/dateutil/rrule.py b/src/libs/python/bongo/external/dateutil/rrule.py deleted file mode 100644 index ec80b81..0000000 --- a/src/libs/python/bongo/external/dateutil/rrule.py +++ /dev/null @@ -1,1090 +0,0 @@ -""" -Copyright (c) 2003-2005 Gustavo Niemeyer - -This module offers extensions to the standard python 2.3+ -datetime module. -""" -__author__ = "Gustavo Niemeyer " -__license__ = "PSF License" - -import itertools -import datetime -import calendar -import _thread -import sys - -__all__ = ["rrule", "rruleset", "rrulestr", - "YEARLY", "MONTHLY", "WEEKLY", "DAILY", - "HOURLY", "MINUTELY", "SECONDLY", - "MO", "TU", "WE", "TH", "FR", "SA", "SU"] - -# Every mask is 7 days longer to handle cross-year weekly periods. -M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30+ - [7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7) -M365MASK = list(M366MASK) -M29, M30, M31 = list(range(1,30)), list(range(1,31)), list(range(1,32)) -MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) -MDAY365MASK = list(MDAY366MASK) -M29, M30, M31 = list(range(-29,0)), list(range(-30,0)), list(range(-31,0)) -NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) -NMDAY365MASK = list(NMDAY366MASK) -M366RANGE = (0,31,60,91,121,152,182,213,244,274,305,335,366) -M365RANGE = (0,31,59,90,120,151,181,212,243,273,304,334,365) -WDAYMASK = [0,1,2,3,4,5,6]*55 -del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31] -MDAY365MASK = tuple(MDAY365MASK) -M365MASK = tuple(M365MASK) - -(YEARLY, - MONTHLY, - WEEKLY, - DAILY, - HOURLY, - MINUTELY, - SECONDLY) = list(range(7)) - -# Imported on demand. -easter = None -parser = None - -class weekday(object): - __slots__ = ["weekday", "n"] - - def __init__(self, weekday, n=None): - if n == 0: - raise ValueError("Can't create weekday with n == 0") - self.weekday = weekday - self.n = n - - def __call__(self, n): - if n == self.n: - return self - else: - return self.__class__(self.weekday, n) - - def __eq__(self, other): - try: - if self.weekday != other.weekday or self.n != other.n: - return False - except AttributeError: - return False - return True - - def __repr__(self): - s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday] - if not self.n: - return s - else: - return "%s(%+d)" % (s, self.n) - -MO, TU, WE, TH, FR, SA, SU = weekdays = tuple([weekday(x) for x in range(7)]) - -class rrulebase: - def __init__(self, cache=False): - if cache: - self._cache = [] - self._cache_lock = _thread.allocate_lock() - self._cache_gen = self._iter() - self._cache_complete = False - else: - self._cache = None - self._cache_complete = False - self._len = None - - def __iter__(self): - if self._cache_complete: - return iter(self._cache) - elif self._cache is None: - return self._iter() - else: - return self._iter_cached() - - def _iter_cached(self): - i = 0 - gen = self._cache_gen - cache = self._cache - acquire = self._cache_lock.acquire - release = self._cache_lock.release - while gen: - if i == len(cache): - acquire() - if self._cache_complete: - break - try: - for j in range(10): - cache.append(next(gen)) - except StopIteration: - self._cache_gen = gen = None - self._cache_complete = True - break - release() - yield cache[i] - i += 1 - while i < self._len: - yield cache[i] - i += 1 - - def __getitem__(self, item): - if self._cache_complete: - return self._cache[item] - elif isinstance(item, slice): - if item.step and item.step < 0: - return list(iter(self))[item] - else: - return list(itertools.islice(self, - item.start or 0, - item.stop or sys.maxsize, - item.step or 1)) - elif item >= 0: - gen = iter(self) - try: - for i in range(item+1): - res = next(gen) - except StopIteration: - raise IndexError - return res - else: - return list(iter(self))[item] - - def __contains__(self, item): - if self._cache_complete: - return item in self._cache - else: - for i in self: - if i == item: - return True - elif i > item: - return False - return False - - # __len__() introduces a large performance penality. - def count(self): - if self._len is None: - for x in self: pass - return self._len - - def before(self, dt, inc=False): - if self._cache_complete: - gen = self._cache - else: - gen = self - last = None - if inc: - for i in gen: - if i > dt: - break - last = i - else: - for i in gen: - if i >= dt: - break - last = i - return last - - def after(self, dt, inc=False): - if self._cache_complete: - gen = self._cache - else: - gen = self - if inc: - for i in gen: - if i >= dt: - return i - else: - for i in gen: - if i > dt: - return i - return None - - def between(self, after, before, inc=False): - if self._cache_complete: - gen = self._cache - else: - gen = self - started = False - l = [] - if inc: - for i in gen: - if i > before: - break - elif not started: - if i >= after: - started = True - l.append(i) - else: - l.append(i) - else: - for i in gen: - if i >= before: - break - elif not started: - if i > after: - started = True - l.append(i) - else: - l.append(i) - return l - -class rrule(rrulebase): - def __init__(self, freq, dtstart=None, - interval=1, wkst=None, count=None, until=None, bysetpos=None, - bymonth=None, bymonthday=None, byyearday=None, byeaster=None, - byweekno=None, byweekday=None, - byhour=None, byminute=None, bysecond=None, - cache=False): - rrulebase.__init__(self, cache) - global easter - if not dtstart: - dtstart = datetime.datetime.now().replace(microsecond=0) - elif not isinstance(dtstart, datetime.datetime): - dtstart = datetime.datetime.fromordinal(dtstart.toordinal()) - else: - dtstart = dtstart.replace(microsecond=0) - self._dtstart = dtstart - self._tzinfo = dtstart.tzinfo - self._freq = freq - self._interval = interval - self._count = count - if until and not isinstance(until, datetime.datetime): - until = datetime.datetime.fromordinal(until.toordinal()) - self._until = until - if wkst is None: - self._wkst = calendar.firstweekday() - elif type(wkst) is int: - self._wkst = wkst - else: - self._wkst = wkst.weekday - if bysetpos is None: - self._bysetpos = None - elif type(bysetpos) is int: - self._bysetpos = (bysetpos,) - else: - self._bysetpos = tuple(bysetpos) - if not (byweekno or byyearday or bymonthday or - byweekday is not None or byeaster is not None): - if freq == YEARLY: - if not bymonth: - bymonth = dtstart.month - bymonthday = dtstart.day - elif freq == MONTHLY: - bymonthday = dtstart.day - elif freq == WEEKLY: - byweekday = dtstart.weekday() - # bymonth - if not bymonth: - self._bymonth = None - elif type(bymonth) is int: - self._bymonth = (bymonth,) - else: - self._bymonth = tuple(bymonth) - # byyearday - if not byyearday: - self._byyearday = None - elif type(byyearday) is int: - self._byyearday = (byyearday,) - else: - self._byyearday = tuple(byyearday) - # byeaster - if byeaster is not None: - if not easter: - from dateutil import easter - if type(byeaster) is int: - self._byeaster = (byeaster,) - else: - self._byeaster = tuple(byeaster) - else: - self._byeaster = None - # bymonthay - if not bymonthday: - self._bymonthday = () - self._bynmonthday = () - elif type(bymonthday) is int: - if bymonthday < 0: - self._bynmonthday = (bymonthday,) - self._bymonthday = () - else: - self._bymonthday = (bymonthday,) - self._bynmonthday = () - else: - self._bymonthday = tuple([x for x in bymonthday if x > 0]) - self._bynmonthday = tuple([x for x in bymonthday if x < 0]) - # byweekno - if byweekno is None: - self._byweekno = None - elif type(byweekno) is int: - self._byweekno = (byweekno,) - else: - self._byweekno = tuple(byweekno) - # byweekday / bynweekday - if byweekday is None: - self._byweekday = None - self._bynweekday = None - elif type(byweekday) is int: - self._byweekday = (byweekday,) - self._bynweekday = None - elif hasattr(byweekday, "n"): - if not byweekday.n or freq > MONTHLY: - self._byweekday = (byweekday.weekday,) - self._bynweekday = None - else: - self._bynweekday = ((byweekday.weekday, byweekday.n),) - self._byweekday = None - else: - self._byweekday = [] - self._bynweekday = [] - for wday in byweekday: - if type(wday) is int: - self._byweekday.append(wday) - elif not wday.n or freq > MONTHLY: - self._byweekday.append(wday.weekday) - else: - self._bynweekday.append((wday.weekday, wday.n)) - self._byweekday = tuple(self._byweekday) - self._bynweekday = tuple(self._bynweekday) - if not self._byweekday: - self._byweekday = None - elif not self._bynweekday: - self._bynweekday = None - # byhour - if byhour is None: - if freq < HOURLY: - self._byhour = (dtstart.hour,) - else: - self._byhour = None - elif type(byhour) is int: - self._byhour = (byhour,) - else: - self._byhour = tuple(byhour) - # byminute - if byminute is None: - if freq < MINUTELY: - self._byminute = (dtstart.minute,) - else: - self._byminute = None - elif type(byminute) is int: - self._byminute = (byminute,) - else: - self._byminute = tuple(byminute) - # bysecond - if bysecond is None: - if freq < SECONDLY: - self._bysecond = (dtstart.second,) - else: - self._bysecond = None - elif type(bysecond) is int: - self._bysecond = (bysecond,) - else: - self._bysecond = tuple(bysecond) - - if self._freq >= HOURLY: - self._timeset = None - else: - self._timeset = [] - for hour in self._byhour: - for minute in self._byminute: - for second in self._bysecond: - self._timeset.append( - datetime.time(hour, minute, second, - tzinfo=self._tzinfo)) - self._timeset.sort() - self._timeset = tuple(self._timeset) - - def _iter(self): - year, month, day, hour, minute, second, weekday, yearday, _ = \ - self._dtstart.timetuple() - - # Some local variables to speed things up a bit - freq = self._freq - interval = self._interval - wkst = self._wkst - until = self._until - bymonth = self._bymonth - byweekno = self._byweekno - byyearday = self._byyearday - byweekday = self._byweekday - byeaster = self._byeaster - bymonthday = self._bymonthday - bynmonthday = self._bynmonthday - bysetpos = self._bysetpos - byhour = self._byhour - byminute = self._byminute - bysecond = self._bysecond - - ii = _iterinfo(self) - ii.rebuild(year, month) - - getdayset = {YEARLY:ii.ydayset, - MONTHLY:ii.mdayset, - WEEKLY:ii.wdayset, - DAILY:ii.ddayset, - HOURLY:ii.ddayset, - MINUTELY:ii.ddayset, - SECONDLY:ii.ddayset}[freq] - - if freq < HOURLY: - timeset = self._timeset - else: - gettimeset = {HOURLY:ii.htimeset, - MINUTELY:ii.mtimeset, - SECONDLY:ii.stimeset}[freq] - if ((freq >= HOURLY and - self._byhour and hour not in self._byhour) or - (freq >= MINUTELY and - self._byminute and minute not in self._byminute) or - (freq >= SECONDLY and - self._bysecond and minute not in self._bysecond)): - timeset = () - else: - timeset = gettimeset(hour, minute, second) - - total = 0 - count = self._count - while True: - # Get dayset with the right frequency - dayset, start, end = getdayset(year, month, day) - - # Do the "hard" work ;-) - filtered = False - for i in dayset[start:end]: - if ((bymonth and ii.mmask[i] not in bymonth) or - (byweekno and not ii.wnomask[i]) or - (byweekday and ii.wdaymask[i] not in byweekday) or - (ii.nwdaymask and not ii.nwdaymask[i]) or - (byeaster and not ii.eastermask[i]) or - ((bymonthday or bynmonthday) and - ii.mdaymask[i] not in bymonthday and - ii.nmdaymask[i] not in bynmonthday) or - (byyearday and - ((i < ii.yearlen and i+1 not in byyearday - and -ii.yearlen+i not in byyearday) or - (i >= ii.yearlen and i+1-ii.yearlen not in byyearday - and -ii.nextyearlen+i-ii.yearlen - not in byyearday)))): - dayset[i] = None - filtered = True - - # Output results - if bysetpos and timeset: - poslist = [] - for pos in bysetpos: - if pos < 0: - daypos, timepos = divmod(pos, len(timeset)) - else: - daypos, timepos = divmod(pos-1, len(timeset)) - try: - i = [x for x in dayset[start:end] - if x is not None][daypos] - time = timeset[timepos] - except IndexError: - pass - else: - date = datetime.date.fromordinal(ii.yearordinal+i) - res = datetime.datetime.combine(date, time) - if res not in poslist: - poslist.append(res) - poslist.sort() - for res in poslist: - if until and res > until: - self._len = total - return - elif res >= self._dtstart: - total += 1 - yield res - if count: - count -= 1 - if not count: - self._len = total - return - else: - for i in dayset[start:end]: - if i is not None: - date = datetime.date.fromordinal(ii.yearordinal+i) - for time in timeset: - res = datetime.datetime.combine(date, time) - if until and res > until: - self._len = total - return - elif res >= self._dtstart: - total += 1 - yield res - if count: - count -= 1 - if not count: - self._len = total - return - - # Handle frequency and interval - fixday = False - if freq == YEARLY: - year += interval - if year > datetime.MAXYEAR: - self._len = total - return - ii.rebuild(year, month) - elif freq == MONTHLY: - month += interval - if month > 12: - div, mod = divmod(month, 12) - month = mod - year += div - if month == 0: - month = 12 - year -= 1 - if year > datetime.MAXYEAR: - self._len = total - return - ii.rebuild(year, month) - elif freq == WEEKLY: - if wkst > weekday: - day += -(weekday+1+(6-wkst))+self._interval*7 - else: - day += -(weekday-wkst)+self._interval*7 - weekday = wkst - fixday = True - elif freq == DAILY: - day += interval - fixday = True - elif freq == HOURLY: - if filtered: - # Jump to one iteration before next day - hour += ((23-hour)//interval)*interval - while True: - hour += interval - div, mod = divmod(hour, 24) - if div: - hour = mod - day += div - fixday = True - if not byhour or hour in byhour: - break - timeset = gettimeset(hour, minute, second) - elif freq == MINUTELY: - if filtered: - # Jump to one iteration before next day - minute += ((1439-(hour*60+minute))//interval)*interval - while True: - minute += interval - div, mod = divmod(minute, 60) - if div: - minute = mod - hour += div - div, mod = divmod(hour, 24) - if div: - hour = mod - day += div - fixday = True - filtered = False - if ((not byhour or hour in byhour) and - (not byminute or minute in byminute)): - break - timeset = gettimeset(hour, minute, second) - elif freq == SECONDLY: - if filtered: - # Jump to one iteration before next day - second += (((86399-(hour*3600+minute*60+second)) - //interval)*interval) - while True: - second += self._interval - div, mod = divmod(second, 60) - if div: - second = mod - minute += div - div, mod = divmod(minute, 60) - if div: - minute = mod - hour += div - div, mod = divmod(hour, 24) - if div: - hour = mod - day += div - fixday = True - if ((not byhour or hour in byhour) and - (not byminute or minute in byminute) and - (not bysecond or second in bysecond)): - break - timeset = gettimeset(hour, minute, second) - - if fixday and day > 28: - daysinmonth = calendar.monthrange(year, month)[1] - if day > daysinmonth: - while day > daysinmonth: - day -= daysinmonth - month += 1 - if month == 13: - month = 1 - year += 1 - if year > datetime.MAXYEAR: - self._len = total - return - daysinmonth = calendar.monthrange(year, month)[1] - ii.rebuild(year, month) - -class _iterinfo(object): - __slots__ = ["rrule", "lastyear", "lastmonth", - "yearlen", "nextyearlen", "yearordinal", "yearweekday", - "mmask", "mrange", "mdaymask", "nmdaymask", - "wdaymask", "wnomask", "nwdaymask", "eastermask"] - - def __init__(self, rrule): - for attr in self.__slots__: - setattr(self, attr, None) - self.rrule = rrule - - def rebuild(self, year, month): - # Every mask is 7 days longer to handle cross-year weekly periods. - rr = self.rrule - if year != self.lastyear: - self.yearlen = 365+calendar.isleap(year) - self.nextyearlen = 365+calendar.isleap(year+1) - firstyday = datetime.date(year, 1, 1) - self.yearordinal = firstyday.toordinal() - self.yearweekday = firstyday.weekday() - - wday = datetime.date(year, 1, 1).weekday() - if self.yearlen == 365: - self.mmask = M365MASK - self.mdaymask = MDAY365MASK - self.nmdaymask = NMDAY365MASK - self.wdaymask = WDAYMASK[wday:] - self.mrange = M365RANGE - else: - self.mmask = M366MASK - self.mdaymask = MDAY366MASK - self.nmdaymask = NMDAY366MASK - self.wdaymask = WDAYMASK[wday:] - self.mrange = M366RANGE - - if not rr._byweekno: - self.wnomask = None - else: - self.wnomask = [0]*(self.yearlen+7) - #no1wkst = firstwkst = self.wdaymask.index(rr._wkst) - no1wkst = firstwkst = (7-self.yearweekday+rr._wkst)%7 - if no1wkst >= 4: - no1wkst = 0 - # Number of days in the year, plus the days we got - # from last year. - wyearlen = self.yearlen+(self.yearweekday-rr._wkst)%7 - else: - # Number of days in the year, minus the days we - # left in last year. - wyearlen = self.yearlen-no1wkst - div, mod = divmod(wyearlen, 7) - numweeks = div+mod//4 - for n in rr._byweekno: - if n < 0: - n += numweeks+1 - if not (0 < n <= numweeks): - continue - if n > 1: - i = no1wkst+(n-1)*7 - if no1wkst != firstwkst: - i -= 7-firstwkst - else: - i = no1wkst - for j in range(7): - self.wnomask[i] = 1 - i += 1 - if self.wdaymask[i] == rr._wkst: - break - if 1 in rr._byweekno: - # Check week number 1 of next year as well - # TODO: Check -numweeks for next year. - i = no1wkst+numweeks*7 - if no1wkst != firstwkst: - i -= 7-firstwkst - if i < self.yearlen: - # If week starts in next year, we - # don't care about it. - for j in range(7): - self.wnomask[i] = 1 - i += 1 - if self.wdaymask[i] == rr._wkst: - break - if no1wkst: - # Check last week number of last year as - # well. If no1wkst is 0, either the year - # started on week start, or week number 1 - # got days from last year, so there are no - # days from last year's last week number in - # this year. - if -1 not in rr._byweekno: - lyearweekday = datetime.date(year-1,1,1).weekday() - lno1wkst = (7-lyearweekday+rr._wkst)%7 - lyearlen = 365+calendar.isleap(year-1) - if lno1wkst >= 4: - lno1wkst = 0 - lnumweeks = 52+(lyearlen+ - (lyearweekday-rr._wkst)%7)%7//4 - else: - lnumweeks = 52+(self.yearlen-no1wkst)%7//4 - else: - lnumweeks = -1 - if lnumweeks in rr._byweekno: - for i in range(no1wkst): - self.wnomask[i] = 1 - - if (rr._bynweekday and - (month != self.lastmonth or year != self.lastyear)): - ranges = [] - if rr._freq == YEARLY: - if rr._bymonth: - for month in rr._bymonth: - ranges.append(self.mrange[month-1:month+1]) - else: - ranges = [(0, self.yearlen)] - elif rr._freq == MONTHLY: - ranges = [self.mrange[month-1:month+1]] - if ranges: - # Weekly frequency won't get here, so we may not - # care about cross-year weekly periods. - self.nwdaymask = [0]*self.yearlen - for first, last in ranges: - last -= 1 - for wday, n in rr._bynweekday: - if n < 0: - i = last+(n+1)*7 - i -= (self.wdaymask[i]-wday)%7 - else: - i = first+(n-1)*7 - i += (7-self.wdaymask[i]+wday)%7 - if first <= i <= last: - self.nwdaymask[i] = 1 - - if rr._byeaster: - self.eastermask = [0]*(self.yearlen+7) - eyday = easter.easter(year).toordinal()-self.yearordinal - for offset in rr._byeaster: - self.eastermask[eyday+offset] = 1 - - self.lastyear = year - self.lastmonth = month - - def ydayset(self, year, month, day): - return list(range(self.yearlen)), 0, self.yearlen - - def mdayset(self, year, month, day): - set = [None]*self.yearlen - start, end = self.mrange[month-1:month+1] - for i in range(start, end): - set[i] = i - return set, start, end - - def wdayset(self, year, month, day): - # We need to handle cross-year weeks here. - set = [None]*(self.yearlen+7) - i = datetime.date(year, month, day).toordinal()-self.yearordinal - start = i - for j in range(7): - set[i] = i - i += 1 - #if (not (0 <= i < self.yearlen) or - # self.wdaymask[i] == self.rrule._wkst): - # This will cross the year boundary, if necessary. - if self.wdaymask[i] == self.rrule._wkst: - break - return set, start, i - - def ddayset(self, year, month, day): - set = [None]*self.yearlen - i = datetime.date(year, month, day).toordinal()-self.yearordinal - set[i] = i - return set, i, i+1 - - def htimeset(self, hour, minute, second): - set = [] - rr = self.rrule - for minute in rr._byminute: - for second in rr._bysecond: - set.append(datetime.time(hour, minute, second, - tzinfo=rr._tzinfo)) - set.sort() - return set - - def mtimeset(self, hour, minute, second): - set = [] - rr = self.rrule - for second in rr._bysecond: - set.append(datetime.time(hour, minute, second, tzinfo=rr._tzinfo)) - set.sort() - return set - - def stimeset(self, hour, minute, second): - return (datetime.time(hour, minute, second, - tzinfo=self.rrule._tzinfo),) - - -class rruleset(rrulebase): - - class _genitem: - def __init__(self, genlist, gen): - try: - self.dt = gen() - genlist.append(self) - except StopIteration: - pass - self.genlist = genlist - self.gen = gen - - def __next__(self): - try: - self.dt = self.gen() - except StopIteration: - self.genlist.remove(self) - - def __cmp__(self, other): - return cmp(self.dt, other.dt) - - def __init__(self, cache=False): - rrulebase.__init__(self, cache) - self._rrule = [] - self._rdate = [] - self._exrule = [] - self._exdate = [] - - def rrule(self, rrule): - self._rrule.append(rrule) - - def rdate(self, rdate): - self._rdate.append(rdate) - - def exrule(self, exrule): - self._exrule.append(exrule) - - def exdate(self, exdate): - self._exdate.append(exdate) - - def _iter(self): - rlist = [] - self._rdate.sort() - self._genitem(rlist, iter(self._rdate).__next__) - for gen in [iter(x).__next__ for x in self._rrule]: - self._genitem(rlist, gen) - rlist.sort() - exlist = [] - self._exdate.sort() - self._genitem(exlist, iter(self._exdate).__next__) - for gen in [iter(x).__next__ for x in self._exrule]: - self._genitem(exlist, gen) - exlist.sort() - lastdt = None - total = 0 - while rlist: - ritem = rlist[0] - if not lastdt or lastdt != ritem.dt: - while exlist and exlist[0] < ritem: - next(exlist[0]) - exlist.sort() - if not exlist or ritem != exlist[0]: - total += 1 - yield ritem.dt - lastdt = ritem.dt - next(ritem) - rlist.sort() - self._len = total - -class _rrulestr: - - _freq_map = {"YEARLY": YEARLY, - "MONTHLY": MONTHLY, - "WEEKLY": WEEKLY, - "DAILY": DAILY, - "HOURLY": HOURLY, - "MINUTELY": MINUTELY, - "SECONDLY": SECONDLY} - - _weekday_map = {"MO":0,"TU":1,"WE":2,"TH":3,"FR":4,"SA":5,"SU":6} - - def _handle_int(self, rrkwargs, name, value, **kwargs): - rrkwargs[name.lower()] = int(value) - - def _handle_int_list(self, rrkwargs, name, value, **kwargs): - rrkwargs[name.lower()] = [int(x) for x in value.split(',')] - - _handle_INTERVAL = _handle_int - _handle_COUNT = _handle_int - _handle_BYSETPOS = _handle_int_list - _handle_BYMONTH = _handle_int_list - _handle_BYMONTHDAY = _handle_int_list - _handle_BYYEARDAY = _handle_int_list - _handle_BYEASTER = _handle_int_list - _handle_BYWEEKNO = _handle_int_list - _handle_BYHOUR = _handle_int_list - _handle_BYMINUTE = _handle_int_list - _handle_BYSECOND = _handle_int_list - - def _handle_FREQ(self, rrkwargs, name, value, **kwargs): - rrkwargs["freq"] = self._freq_map[value] - - def _handle_UNTIL(self, rrkwargs, name, value, **kwargs): - global parser - if not parser: - from dateutil import parser - try: - rrkwargs["until"] = parser.parse(value, - ignoretz=kwargs.get("ignoretz"), - tzinfos=kwargs.get("tzinfos")) - except ValueError: - raise ValueError("invalid until date") - - def _handle_WKST(self, rrkwargs, name, value, **kwargs): - rrkwargs["wkst"] = self._weekday_map[value] - - def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwarsg): - l = [] - for wday in value.split(','): - for i in range(len(wday)): - if wday[i] not in '+-0123456789': - break - n = wday[:i] or None - w = wday[i:] - if n: n = int(n) - l.append(weekdays[self._weekday_map[w]](n)) - rrkwargs["byweekday"] = l - - _handle_BYDAY = _handle_BYWEEKDAY - - def _parse_rfc_rrule(self, line, - dtstart=None, - cache=False, - ignoretz=False, - tzinfos=None): - if line.find(':') != -1: - name, value = line.split(':') - if name != "RRULE": - raise ValueError("unknown parameter name") - else: - value = line - rrkwargs = {} - for pair in value.split(';'): - name, value = pair.split('=') - name = name.upper() - value = value.upper() - try: - getattr(self, "_handle_"+name)(rrkwargs, name, value, - ignoretz=ignoretz, - tzinfos=tzinfos) - except AttributeError: - raise "unknown parameter '%s'" % name - except (KeyError, ValueError): - raise "invalid '%s': %s" % (name, value) - return rrule(dtstart=dtstart, cache=cache, **rrkwargs) - - def _parse_rfc(self, s, - dtstart=None, - cache=False, - unfold=False, - forceset=False, - compatible=False, - ignoretz=False, - tzinfos=None): - global parser - if compatible: - forceset = True - unfold = True - s = s.upper() - if not s.strip(): - raise ValueError("empty string") - if unfold: - lines = s.splitlines() - i = 0 - while i < len(lines): - line = lines[i].rstrip() - if not line: - del lines[i] - elif i > 0 and line[0] == " ": - lines[i-1] += line[1:] - del lines[i] - else: - i += 1 - else: - lines = s.split() - if (not forceset and len(lines) == 1 and - (s.find(':') == -1 or s.startswith('RRULE:'))): - return self._parse_rfc_rrule(lines[0], cache=cache, - dtstart=dtstart, ignoretz=ignoretz, - tzinfos=tzinfos) - else: - rrulevals = [] - rdatevals = [] - exrulevals = [] - exdatevals = [] - for line in lines: - if not line: - continue - if line.find(':') == -1: - name = "RRULE" - value = line - else: - name, value = line.split(':', 1) - parms = name.split(';') - if not parms: - raise ValueError("empty property name") - name = parms[0] - parms = parms[1:] - if name == "RRULE": - for parm in parms: - raise ValueError("unsupported RRULE parm: "+parm) - rrulevals.append(value) - elif name == "RDATE": - for parm in parms: - if parm != "VALUE=DATE-TIME": - raise ValueError("unsupported RDATE parm: "+parm) - rdatevals.append(value) - elif name == "EXRULE": - for parm in parms: - raise ValueError("unsupported EXRULE parm: "+parm) - exrulevals.append(value) - elif name == "EXDATE": - for parm in parms: - if parm != "VALUE=DATE-TIME": - raise ValueError("unsupported RDATE parm: "+parm) - exdatevals.append(value) - elif name == "DTSTART": - for parm in parms: - raise ValueError("unsupported DTSTART parm: "+parm) - if not parser: - from dateutil import parser - dtstart = parser.parse(value, ignoretz=ignoretz, - tzinfos=tzinfos) - else: - raise ValueError("unsupported property: "+name) - if (forceset or len(rrulevals) > 1 or - rdatevals or exrulevals or exdatevals): - if not parser and (rdatevals or exdatevals): - from dateutil import parser - set = rruleset(cache=cache) - for value in rrulevals: - set.rrule(self._parse_rfc_rrule(value, dtstart=dtstart, - ignoretz=ignoretz, - tzinfos=tzinfos)) - for value in rdatevals: - for datestr in value.split(','): - set.rdate(parser.parse(datestr, - ignoretz=ignoretz, - tzinfos=tzinfos)) - for value in exrulevals: - set.exrule(self._parse_rfc_rrule(value, dtstart=dtstart, - ignoretz=ignoretz, - tzinfos=tzinfos)) - for value in exdatevals: - for datestr in value.split(','): - set.exdate(parser.parse(datestr, - ignoretz=ignoretz, - tzinfos=tzinfos)) - if compatible and dtstart: - set.rdate(dtstart) - return set - else: - return self._parse_rfc_rrule(rrulevals[0], - dtstart=dtstart, - cache=cache, - ignoretz=ignoretz, - tzinfos=tzinfos) - - def __call__(self, s, **kwargs): - return self._parse_rfc(s, **kwargs) - -rrulestr = _rrulestr() - -# vim:ts=4:sw=4:et diff --git a/src/libs/python/bongo/external/dateutil/tz.py b/src/libs/python/bongo/external/dateutil/tz.py deleted file mode 100644 index 39839e3..0000000 --- a/src/libs/python/bongo/external/dateutil/tz.py +++ /dev/null @@ -1,924 +0,0 @@ -""" -Copyright (c) 2003-2005 Gustavo Niemeyer - -This module offers extensions to the standard python 2.3+ -datetime module. -""" -__author__ = "Gustavo Niemeyer " -__license__ = "PSF License" - -import datetime -import struct -import time -import sys -import os - -relativedelta = None -parser = None -rrule = None - -__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange", - "tzstr", "tzical", "tzwin", "tzwinlocal", "gettz"] - -try: - from dateutil.tzwin import tzwin, tzwinlocal -except (ImportError, OSError): - tzwin, tzwinlocal = None, None - -ZERO = datetime.timedelta(0) -EPOCHORDINAL = datetime.datetime.utcfromtimestamp(0).toordinal() - -class tzutc(datetime.tzinfo): - - def utcoffset(self, dt): - return ZERO - - def dst(self, dt): - return ZERO - - def tzname(self, dt): - return "UTC" - - def __eq__(self, other): - return (isinstance(other, tzutc) or - (isinstance(other, tzoffset) and other._offset == ZERO)) - - def __ne__(self, other): - return not self.__eq__(other) - - def __repr__(self): - return "%s()" % self.__class__.__name__ - - __reduce__ = object.__reduce__ - -class tzoffset(datetime.tzinfo): - - def __init__(self, name, offset): - self._name = name - self._offset = datetime.timedelta(seconds=offset) - - def utcoffset(self, dt): - return self._offset - - def dst(self, dt): - return ZERO - - def tzname(self, dt): - return self._name - - def __eq__(self, other): - return (isinstance(other, tzoffset) and - self._offset == other._offset) - - def __ne__(self, other): - return not self.__eq__(other) - - def __repr__(self): - return "%s(%s, %s)" % (self.__class__.__name__, - repr(self._name), - self._offset.days*86400+self._offset.seconds) - - __reduce__ = object.__reduce__ - -class tzlocal(datetime.tzinfo): - - _std_offset = datetime.timedelta(seconds=-time.timezone) - if time.daylight: - _dst_offset = datetime.timedelta(seconds=-time.altzone) - else: - _dst_offset = _std_offset - - def utcoffset(self, dt): - if self._isdst(dt): - return self._dst_offset - else: - return self._std_offset - - def dst(self, dt): - if self._isdst(dt): - return self._dst_offset-self._std_offset - else: - return ZERO - - def tzname(self, dt): - return time.tzname[self._isdst(dt)] - - def _isdst(self, dt): - # We can't use mktime here. It is unstable when deciding if - # the hour near to a change is DST or not. - # - # timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour, - # dt.minute, dt.second, dt.weekday(), 0, -1)) - # return time.localtime(timestamp).tm_isdst - # - # The code above yields the following result: - # - #>>> import tz, datetime - #>>> t = tz.tzlocal() - #>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() - #'BRDT' - #>>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname() - #'BRST' - #>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() - #'BRST' - #>>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname() - #'BRDT' - #>>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() - #'BRDT' - # - # Here is a more stable implementation: - # - timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400 - + dt.hour * 3600 - + dt.minute * 60 - + dt.second) - return time.localtime(timestamp+time.timezone).tm_isdst - - def __eq__(self, other): - if not isinstance(other, tzlocal): - return False - return (self._std_offset == other._std_offset and - self._dst_offset == other._dst_offset) - return True - - def __ne__(self, other): - return not self.__eq__(other) - - def __repr__(self): - return "%s()" % self.__class__.__name__ - - __reduce__ = object.__reduce__ - -class _ttinfo(object): - __slots__ = ["offset", "delta", "isdst", "abbr", "isstd", "isgmt"] - - def __init__(self): - for attr in self.__slots__: - setattr(self, attr, None) - - def __repr__(self): - l = [] - for attr in self.__slots__: - value = getattr(self, attr) - if value is not None: - l.append("%s=%s" % (attr, repr(value))) - return "%s(%s)" % (self.__class__.__name__, ", ".join(l)) - - def __eq__(self, other): - if not isinstance(other, _ttinfo): - return False - return (self.offset == other.offset and - self.delta == other.delta and - self.isdst == other.isdst and - self.abbr == other.abbr and - self.isstd == other.isstd and - self.isgmt == other.isgmt) - - def __ne__(self, other): - return not self.__eq__(other) - - def __getstate__(self): - state = {} - for name in self.__slots__: - state[name] = getattr(self, name, None) - return state - - def __setstate__(self, state): - for name in self.__slots__: - if name in state: - setattr(self, name, state[name]) - -class tzfile(datetime.tzinfo): - - # http://www.twinsun.com/tz/tz-link.htm - # ftp://elsie.nci.nih.gov/pub/tz*.tar.gz - - def __init__(self, fileobj): - if isinstance(fileobj, str): - self._filename = fileobj - fileobj = open(fileobj) - elif hasattr(fileobj, "name"): - self._filename = fileobj.name - else: - self._filename = repr(fileobj) - - # From tzfile(5): - # - # The time zone information files used by tzset(3) - # begin with the magic characters "TZif" to identify - # them as time zone information files, followed by - # sixteen bytes reserved for future use, followed by - # six four-byte values of type long, written in a - # ``standard'' byte order (the high-order byte - # of the value is written first). - - if fileobj.read(4) != "TZif": - raise ValueError("magic not found") - - fileobj.read(16) - - ( - # The number of UTC/local indicators stored in the file. - ttisgmtcnt, - - # The number of standard/wall indicators stored in the file. - ttisstdcnt, - - # The number of leap seconds for which data is - # stored in the file. - leapcnt, - - # The number of "transition times" for which data - # is stored in the file. - timecnt, - - # The number of "local time types" for which data - # is stored in the file (must not be zero). - typecnt, - - # The number of characters of "time zone - # abbreviation strings" stored in the file. - charcnt, - - ) = struct.unpack(">6l", fileobj.read(24)) - - # The above header is followed by tzh_timecnt four-byte - # values of type long, sorted in ascending order. - # These values are written in ``standard'' byte order. - # Each is used as a transition time (as returned by - # time(2)) at which the rules for computing local time - # change. - - if timecnt: - self._trans_list = struct.unpack(">%dl" % timecnt, - fileobj.read(timecnt*4)) - else: - self._trans_list = [] - - # Next come tzh_timecnt one-byte values of type unsigned - # char; each one tells which of the different types of - # ``local time'' types described in the file is associated - # with the same-indexed transition time. These values - # serve as indices into an array of ttinfo structures that - # appears next in the file. - - if timecnt: - self._trans_idx = struct.unpack(">%dB" % timecnt, - fileobj.read(timecnt)) - else: - self._trans_idx = [] - - # Each ttinfo structure is written as a four-byte value - # for tt_gmtoff of type long, in a standard byte - # order, followed by a one-byte value for tt_isdst - # and a one-byte value for tt_abbrind. In each - # structure, tt_gmtoff gives the number of - # seconds to be added to UTC, tt_isdst tells whether - # tm_isdst should be set by localtime(3), and - # tt_abbrind serves as an index into the array of - # time zone abbreviation characters that follow the - # ttinfo structure(s) in the file. - - ttinfo = [] - - for i in range(typecnt): - ttinfo.append(struct.unpack(">lbb", fileobj.read(6))) - - abbr = fileobj.read(charcnt) - - # Then there are tzh_leapcnt pairs of four-byte - # values, written in standard byte order; the - # first value of each pair gives the time (as - # returned by time(2)) at which a leap second - # occurs; the second gives the total number of - # leap seconds to be applied after the given time. - # The pairs of values are sorted in ascending order - # by time. - - # Not used, for now - if leapcnt: - leap = struct.unpack(">%dl" % leapcnt*2, - fileobj.read(leapcnt*8)) - - # Then there are tzh_ttisstdcnt standard/wall - # indicators, each stored as a one-byte value; - # they tell whether the transition times associated - # with local time types were specified as standard - # time or wall clock time, and are used when - # a time zone file is used in handling POSIX-style - # time zone environment variables. - - if ttisstdcnt: - isstd = struct.unpack(">%db" % ttisstdcnt, - fileobj.read(ttisstdcnt)) - - # Finally, there are tzh_ttisgmtcnt UTC/local - # indicators, each stored as a one-byte value; - # they tell whether the transition times associated - # with local time types were specified as UTC or - # local time, and are used when a time zone file - # is used in handling POSIX-style time zone envi- - # ronment variables. - - if ttisgmtcnt: - isgmt = struct.unpack(">%db" % ttisgmtcnt, - fileobj.read(ttisgmtcnt)) - - # ** Everything has been read ** - - # Build ttinfo list - self._ttinfo_list = [] - for i in range(typecnt): - tti = _ttinfo() - tti.offset = ttinfo[i][0] - tti.delta = datetime.timedelta(seconds=ttinfo[i][0]) - tti.isdst = ttinfo[i][1] - tti.abbr = abbr[ttinfo[i][2]:abbr.find('\x00', ttinfo[i][2])] - tti.isstd = (ttisstdcnt > i and isstd[i] != 0) - tti.isgmt = (ttisgmtcnt > i and isgmt[i] != 0) - self._ttinfo_list.append(tti) - - # Replace ttinfo indexes for ttinfo objects. - trans_idx = [] - for idx in self._trans_idx: - trans_idx.append(self._ttinfo_list[idx]) - self._trans_idx = tuple(trans_idx) - - # Set standard, dst, and before ttinfos. before will be - # used when a given time is before any transitions, - # and will be set to the first non-dst ttinfo, or to - # the first dst, if all of them are dst. - self._ttinfo_std = None - self._ttinfo_dst = None - self._ttinfo_before = None - if self._ttinfo_list: - if not self._trans_list: - self._ttinfo_std = self._ttinfo_first = self._ttinfo_list[0] - else: - for i in range(timecnt-1,-1,-1): - tti = self._trans_idx[i] - if not self._ttinfo_std and not tti.isdst: - self._ttinfo_std = tti - elif not self._ttinfo_dst and tti.isdst: - self._ttinfo_dst = tti - if self._ttinfo_std and self._ttinfo_dst: - break - else: - if self._ttinfo_dst and not self._ttinfo_std: - self._ttinfo_std = self._ttinfo_dst - - for tti in self._ttinfo_list: - if not tti.isdst: - self._ttinfo_before = tti - break - else: - self._ttinfo_before = self._ttinfo_list[0] - - # Now fix transition times to become relative to wall time. - # - # I'm not sure about this. In my tests, the tz source file - # is setup to wall time, and in the binary file isstd and - # isgmt are off, so it should be in wall time. OTOH, it's - # always in gmt time. Let me know if you have comments - # about this. - laststdoffset = 0 - self._trans_list = list(self._trans_list) - for i in range(len(self._trans_list)): - tti = self._trans_idx[i] - if not tti.isdst: - # This is std time. - self._trans_list[i] += tti.offset - laststdoffset = tti.offset - else: - # This is dst time. Convert to std. - self._trans_list[i] += laststdoffset - self._trans_list = tuple(self._trans_list) - - def _find_ttinfo(self, dt, laststd=0): - timestamp = ((dt.toordinal() - EPOCHORDINAL) * 86400 - + dt.hour * 3600 - + dt.minute * 60 - + dt.second) - idx = 0 - for trans in self._trans_list: - if timestamp < trans: - break - idx += 1 - else: - return self._ttinfo_std - if idx == 0: - return self._ttinfo_before - if laststd: - while idx > 0: - tti = self._trans_idx[idx-1] - if not tti.isdst: - return tti - idx -= 1 - else: - return self._ttinfo_std - else: - return self._trans_idx[idx-1] - - def utcoffset(self, dt): - if not self._ttinfo_std: - return ZERO - return self._find_ttinfo(dt).delta - - def dst(self, dt): - if not self._ttinfo_dst: - return ZERO - tti = self._find_ttinfo(dt) - if not tti.isdst: - return ZERO - - # The documentation says that utcoffset()-dst() must - # be constant for every dt. - return self._find_ttinfo(dt, laststd=1).delta-tti.delta - - # An alternative for that would be: - # - # return self._ttinfo_dst.offset-self._ttinfo_std.offset - # - # However, this class stores historical changes in the - # dst offset, so I belive that this wouldn't be the right - # way to implement this. - - def tzname(self, dt): - if not self._ttinfo_std: - return None - return self._find_ttinfo(dt).abbr - - def __eq__(self, other): - if not isinstance(other, tzfile): - return False - return (self._trans_list == other._trans_list and - self._trans_idx == other._trans_idx and - self._ttinfo_list == other._ttinfo_list) - - def __ne__(self, other): - return not self.__eq__(other) - - - def __repr__(self): - return "%s(%s)" % (self.__class__.__name__, repr(self._filename)) - - def __reduce__(self): - if not os.path.isfile(self._filename): - raise ValueError("Unpickable %s class" % self.__class__.__name__) - return (self.__class__, (self._filename,)) - -class tzrange(datetime.tzinfo): - - def __init__(self, stdabbr, stdoffset=None, - dstabbr=None, dstoffset=None, - start=None, end=None): - global relativedelta - if not relativedelta: - from dateutil import relativedelta - self._std_abbr = stdabbr - self._dst_abbr = dstabbr - if stdoffset is not None: - self._std_offset = datetime.timedelta(seconds=stdoffset) - else: - self._std_offset = ZERO - if dstoffset is not None: - self._dst_offset = datetime.timedelta(seconds=dstoffset) - elif dstabbr and stdoffset is not None: - self._dst_offset = self._std_offset+datetime.timedelta(hours=+1) - else: - self._dst_offset = ZERO - if start is None: - self._start_delta = relativedelta.relativedelta( - hours=+2, month=4, day=1, weekday=relativedelta.SU(+1)) - else: - self._start_delta = start - if end is None: - self._end_delta = relativedelta.relativedelta( - hours=+1, month=10, day=31, weekday=relativedelta.SU(-1)) - else: - self._end_delta = end - - def utcoffset(self, dt): - if self._isdst(dt): - return self._dst_offset - else: - return self._std_offset - - def dst(self, dt): - if self._isdst(dt): - return self._dst_offset-self._std_offset - else: - return ZERO - - def tzname(self, dt): - if self._isdst(dt): - return self._dst_abbr - else: - return self._std_abbr - - def _isdst(self, dt): - if not self._start_delta: - return False - year = datetime.date(dt.year,1,1) - start = year+self._start_delta - end = year+self._end_delta - dt = dt.replace(tzinfo=None) - if start < end: - return dt >= start and dt < end - else: - return dt >= start or dt < end - - def __eq__(self, other): - if not isinstance(other, tzrange): - return False - return (self._std_abbr == other._std_abbr and - self._dst_abbr == other._dst_abbr and - self._std_offset == other._std_offset and - self._dst_offset == other._dst_offset and - self._start_delta == other._start_delta and - self._end_delta == other._end_delta) - - def __ne__(self, other): - return not self.__eq__(other) - - def __repr__(self): - return "%s(...)" % self.__class__.__name__ - - __reduce__ = object.__reduce__ - -class tzstr(tzrange): - - def __init__(self, s): - global parser - if not parser: - from dateutil import parser - self._s = s - - res = parser._parsetz(s) - if res is None: - raise ValueError("unknown string format") - - # We must initialize it first, since _delta() needs - # _std_offset and _dst_offset set. Use False in start/end - # to avoid building it two times. - tzrange.__init__(self, res.stdabbr, res.stdoffset, - res.dstabbr, res.dstoffset, - start=False, end=False) - - self._start_delta = self._delta(res.start) - if self._start_delta: - self._end_delta = self._delta(res.end, isend=1) - - def _delta(self, x, isend=0): - kwargs = {} - if x.month is not None: - kwargs["month"] = x.month - if x.weekday is not None: - kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week) - if x.week > 0: - kwargs["day"] = 1 - else: - kwargs["day"] = 31 - elif x.day: - kwargs["day"] = x.day - elif x.yday is not None: - kwargs["yearday"] = x.yday - elif x.jyday is not None: - kwargs["nlyearday"] = x.jyday - if not kwargs: - # Default is to start on first sunday of april, and end - # on last sunday of october. - if not isend: - kwargs["month"] = 4 - kwargs["day"] = 1 - kwargs["weekday"] = relativedelta.SU(+1) - else: - kwargs["month"] = 10 - kwargs["day"] = 31 - kwargs["weekday"] = relativedelta.SU(-1) - if x.time is not None: - kwargs["seconds"] = x.time - else: - # Default is 2AM. - kwargs["seconds"] = 7200 - if isend: - # Convert to standard time, to follow the documented way - # of working with the extra hour. See the documentation - # of the tzinfo class. - delta = self._dst_offset-self._std_offset - kwargs["seconds"] -= delta.seconds+delta.days*86400 - return relativedelta.relativedelta(**kwargs) - - def __repr__(self): - return "%s(%s)" % (self.__class__.__name__, repr(self._s)) - -class _tzicalvtzcomp: - def __init__(self, tzoffsetfrom, tzoffsetto, isdst, - tzname=None, rrule=None): - self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom) - self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto) - self.tzoffsetdiff = self.tzoffsetto-self.tzoffsetfrom - self.isdst = isdst - self.tzname = tzname - self.rrule = rrule - -class _tzicalvtz(datetime.tzinfo): - def __init__(self, tzid, comps=[]): - self._tzid = tzid - self._comps = comps - self._cachedate = [] - self._cachecomp = [] - - def _find_comp(self, dt): - if len(self._comps) == 1: - return self._comps[0] - dt = dt.replace(tzinfo=None) - try: - return self._cachecomp[self._cachedate.index(dt)] - except ValueError: - pass - lastcomp = None - lastcompdt = None - for comp in self._comps: - if not comp.isdst: - # Handle the extra hour in DST -> STD - compdt = comp.rrule.before(dt-comp.tzoffsetdiff, inc=True) - else: - compdt = comp.rrule.before(dt, inc=True) - if compdt and (not lastcompdt or lastcompdt < compdt): - lastcompdt = compdt - lastcomp = comp - if not lastcomp: - # RFC says nothing about what to do when a given - # time is before the first onset date. We'll look for the - # first standard component, or the first component, if - # none is found. - for comp in self._comps: - if not comp.isdst: - lastcomp = comp - break - else: - lastcomp = comp[0] - self._cachedate.insert(0, dt) - self._cachecomp.insert(0, lastcomp) - if len(self._cachedate) > 10: - self._cachedate.pop() - self._cachecomp.pop() - return lastcomp - - def utcoffset(self, dt): - return self._find_comp(dt).tzoffsetto - - def dst(self, dt): - comp = self._find_comp(dt) - if comp.isdst: - return comp.tzoffsetdiff - else: - return ZERO - - def tzname(self, dt): - return self._find_comp(dt).tzname - - def __repr__(self): - return "" % repr(self._tzid) - - __reduce__ = object.__reduce__ - -class tzical: - def __init__(self, fileobj): - global rrule - if not rrule: - from dateutil import rrule - - if isinstance(fileobj, str): - self._s = fileobj - fileobj = open(fileobj) - elif hasattr(fileobj, "name"): - self._s = fileobj.name - else: - self._s = repr(fileobj) - - self._vtz = {} - - self._parse_rfc(fileobj.read()) - - def keys(self): - return list(self._vtz.keys()) - - def get(self, tzid=None): - if tzid is None: - keys = list(self._vtz.keys()) - if len(keys) == 0: - raise "no timezones defined" - elif len(keys) > 1: - raise "more than one timezone available" - tzid = keys[0] - return self._vtz.get(tzid) - - def _parse_offset(self, s): - s = s.strip() - if not s: - raise ValueError("empty offset") - if s[0] in ('+', '-'): - signal = (-1,+1)[s[0]=='+'] - s = s[1:] - else: - signal = +1 - if len(s) == 4: - return (int(s[:2])*3600+int(s[2:])*60)*signal - elif len(s) == 6: - return (int(s[:2])*3600+int(s[2:4])*60+int(s[4:]))*signal - else: - raise ValueError("invalid offset: "+s) - - def _parse_rfc(self, s): - lines = s.splitlines() - if not lines: - raise ValueError("empty string") - - # Unfold - i = 0 - while i < len(lines): - line = lines[i].rstrip() - if not line: - del lines[i] - elif i > 0 and line[0] == " ": - lines[i-1] += line[1:] - del lines[i] - else: - i += 1 - - invtz = False - comptype = None - for line in lines: - if not line: - continue - name, value = line.split(':', 1) - parms = name.split(';') - if not parms: - raise ValueError("empty property name") - name = parms[0].upper() - parms = parms[1:] - if invtz: - if name == "BEGIN": - if value in ("STANDARD", "DAYLIGHT"): - # Process component - pass - else: - raise ValueError("unknown component: "+value) - comptype = value - founddtstart = False - tzoffsetfrom = None - tzoffsetto = None - rrulelines = [] - tzname = None - elif name == "END": - if value == "VTIMEZONE": - if comptype: - raise ValueError("component not closed: "+comptype) - if not tzid: - raise ValueError("mandatory TZID not found") - if not comps: - raise ValueError("at least one component is needed") - # Process vtimezone - self._vtz[tzid] = _tzicalvtz(tzid, comps) - invtz = False - elif value == comptype: - if not founddtstart: - raise ValueError("mandatory DTSTART not found") - if tzoffsetfrom is None: - raise ValueError("mandatory TZOFFSETFROM not found") - if tzoffsetto is None: - raise ValueError("mandatory TZOFFSETFROM not found") - # Process component - rr = None - if rrulelines: - rr = rrule.rrulestr("\n".join(rrulelines), - compatible=True, - ignoretz=True, - cache=True) - comp = _tzicalvtzcomp(tzoffsetfrom, tzoffsetto, - (comptype == "DAYLIGHT"), - tzname, rr) - comps.append(comp) - comptype = None - else: - raise ValueError("invalid component end: "+value) - elif comptype: - if name == "DTSTART": - rrulelines.append(line) - founddtstart = True - elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"): - rrulelines.append(line) - elif name == "TZOFFSETFROM": - if parms: - raise ValueError("unsupported %s parm: %s "%(name, parms[0])) - tzoffsetfrom = self._parse_offset(value) - elif name == "TZOFFSETTO": - if parms: - raise ValueError("unsupported TZOFFSETTO parm: "+parms[0]) - tzoffsetto = self._parse_offset(value) - elif name == "TZNAME": - if parms: - raise ValueError("unsupported TZNAME parm: "+parms[0]) - tzname = value - elif name == "COMMENT": - pass - else: - raise ValueError("unsupported property: "+name) - else: - if name == "TZID": - if parms: - raise ValueError("unsupported TZID parm: "+parms[0]) - tzid = value - elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"): - pass - else: - raise ValueError("unsupported property: "+name) - elif name == "BEGIN" and value == "VTIMEZONE": - tzid = None - comps = [] - invtz = True - - def __repr__(self): - return "%s(%s)" % (self.__class__.__name__, repr(self._s)) - -if sys.platform != "win32": - TZFILES = ["/etc/localtime", "localtime"] - TZPATHS = ["/usr/share/zoneinfo", "/usr/lib/zoneinfo", "/etc/zoneinfo"] -else: - TZFILES = [] - TZPATHS = [] - -def gettz(name=None): - tz = None - if not name: - try: - name = os.environ["TZ"] - except KeyError: - pass - if name is None or name == ":": - for filepath in TZFILES: - if not os.path.isabs(filepath): - filename = filepath - for path in TZPATHS: - filepath = os.path.join(path, filename) - if os.path.isfile(filepath): - break - else: - continue - if os.path.isfile(filepath): - try: - tz = tzfile(filepath) - break - except (IOError, OSError, ValueError): - pass - else: - tz = tzlocal() - else: - if name.startswith(":"): - name = name[:-1] - if os.path.isabs(name): - if os.path.isfile(name): - tz = tzfile(name) - else: - tz = None - else: - for path in TZPATHS: - filepath = os.path.join(path, name) - if not os.path.isfile(filepath): - filepath = filepath.replace(' ','_') - if not os.path.isfile(filepath): - continue - try: - tz = tzfile(filepath) - break - except (IOError, OSError, ValueError): - pass - else: - tz = None - if tzwin: - try: - tz = tzwin(name) - except OSError: - pass - if not tz: - from dateutil.zoneinfo import gettz - tz = gettz(name) - if not tz: - for c in name: - # name must have at least one offset to be a tzstr - if c in "0123456789": - try: - tz = tzstr(name) - except ValueError: - pass - break - else: - if name in ("GMT", "UTC"): - tz = tzutc() - elif name in time.tzname: - tz = tzlocal() - return tz - -# vim:ts=4:sw=4:et diff --git a/src/libs/python/bongo/external/dateutil/tzwin.py b/src/libs/python/bongo/external/dateutil/tzwin.py deleted file mode 100644 index b378f59..0000000 --- a/src/libs/python/bongo/external/dateutil/tzwin.py +++ /dev/null @@ -1,180 +0,0 @@ -# This code was originally contributed by Jeffrey Harris. -import datetime -import struct -import winreg - -__author__ = "Jeffrey Harris & Gustavo Niemeyer " - -__all__ = ["tzwin", "tzwinlocal"] - -ONEWEEK = datetime.timedelta(7) - -TZKEYNAMENT = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones" -TZKEYNAME9X = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones" -TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation" - -def _settzkeyname(): - global TZKEYNAME - handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) - try: - winreg.OpenKey(handle, TZKEYNAMENT).Close() - TZKEYNAME = TZKEYNAMENT - except WindowsError: - TZKEYNAME = TZKEYNAME9X - handle.Close() - -_settzkeyname() - -class tzwinbase(datetime.tzinfo): - """tzinfo class based on win32's timezones available in the registry.""" - - def utcoffset(self, dt): - if self._isdst(dt): - return datetime.timedelta(minutes=self._dstoffset) - else: - return datetime.timedelta(minutes=self._stdoffset) - - def dst(self, dt): - if self._isdst(dt): - minutes = self._dstoffset - self._stdoffset - return datetime.timedelta(minutes=minutes) - else: - return datetime.timedelta(0) - - def tzname(self, dt): - if self._isdst(dt): - return self._dstname - else: - return self._stdname - - def list(): - """Return a list of all time zones known to the system.""" - handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) - tzkey = winreg.OpenKey(handle, TZKEYNAME) - result = [winreg.EnumKey(tzkey, i) - for i in range(winreg.QueryInfoKey(tzkey)[0])] - tzkey.Close() - handle.Close() - return result - list = staticmethod(list) - - def display(self): - return self._display - - def _isdst(self, dt): - dston = picknthweekday(dt.year, self._dstmonth, self._dstdayofweek, - self._dsthour, self._dstminute, - self._dstweeknumber) - dstoff = picknthweekday(dt.year, self._stdmonth, self._stddayofweek, - self._stdhour, self._stdminute, - self._stdweeknumber) - if dston < dstoff: - return dston <= dt.replace(tzinfo=None) < dstoff - else: - return not dstoff <= dt.replace(tzinfo=None) < dston - - -class tzwin(tzwinbase): - - def __init__(self, name): - self._name = name - - handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) - tzkey = winreg.OpenKey(handle, "%s\%s" % (TZKEYNAME, name)) - keydict = valuestodict(tzkey) - tzkey.Close() - handle.Close() - - self._stdname = keydict["Std"].encode("iso-8859-1") - self._dstname = keydict["Dlt"].encode("iso-8859-1") - - self._display = keydict["Display"] - - # See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm - tup = struct.unpack("=3l16h", keydict["TZI"]) - self._stdoffset = -tup[0]-tup[1] # Bias + StandardBias * -1 - self._dstoffset = self._stdoffset-tup[2] # + DaylightBias * -1 - - (self._stdmonth, - self._stddayofweek, # Sunday = 0 - self._stdweeknumber, # Last = 5 - self._stdhour, - self._stdminute) = tup[4:9] - - (self._dstmonth, - self._dstdayofweek, # Sunday = 0 - self._dstweeknumber, # Last = 5 - self._dsthour, - self._dstminute) = tup[12:17] - - def __repr__(self): - return "tzwin(%s)" % repr(self._name) - - def __reduce__(self): - return (self.__class__, (self._name,)) - - -class tzwinlocal(tzwinbase): - - def __init__(self): - - handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) - - tzlocalkey = winreg.OpenKey(handle, TZLOCALKEYNAME) - keydict = valuestodict(tzlocalkey) - tzlocalkey.Close() - - self._stdname = keydict["StandardName"].encode("iso-8859-1") - self._dstname = keydict["DaylightName"].encode("iso-8859-1") - - try: - tzkey = winreg.OpenKey(handle, "%s\%s"%(TZKEYNAME, self._stdname)) - _keydict = valuestodict(tzkey) - self._display = _keydict["Display"] - tzkey.Close() - except OSError: - self._display = None - - handle.Close() - - self._stdoffset = -keydict["Bias"]-keydict["StandardBias"] - self._dstoffset = self._stdoffset-keydict["DaylightBias"] - - - # See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm - tup = struct.unpack("=8h", keydict["StandardStart"]) - - (self._stdmonth, - self._stddayofweek, # Sunday = 0 - self._stdweeknumber, # Last = 5 - self._stdhour, - self._stdminute) = tup[1:6] - - tup = struct.unpack("=8h", keydict["DaylightStart"]) - - (self._dstmonth, - self._dstdayofweek, # Sunday = 0 - self._dstweeknumber, # Last = 5 - self._dsthour, - self._dstminute) = tup[1:6] - - def __reduce__(self): - return (self.__class__, ()) - -def picknthweekday(year, month, dayofweek, hour, minute, whichweek): - """dayofweek == 0 means Sunday, whichweek 5 means last instance""" - first = datetime.datetime(year, month, 1, hour, minute) - weekdayone = first.replace(day=((dayofweek-first.isoweekday())%7+1)) - for n in range(whichweek): - dt = weekdayone+(whichweek-n)*ONEWEEK - if dt.month == month: - return dt - -def valuestodict(key): - """Convert a registry key's values to a dictionary.""" - dict = {} - size = winreg.QueryInfoKey(key)[1] - for i in range(size): - data = winreg.EnumValue(key, i) - dict[data[0]] = data[1] - return dict diff --git a/src/libs/python/bongo/external/dateutil/zoneinfo/__init__.py b/src/libs/python/bongo/external/dateutil/zoneinfo/__init__.py deleted file mode 100644 index 856fcf1..0000000 --- a/src/libs/python/bongo/external/dateutil/zoneinfo/__init__.py +++ /dev/null @@ -1,80 +0,0 @@ -""" -Copyright (c) 2003-2005 Gustavo Niemeyer - -This module offers extensions to the standard python 2.3+ -datetime module. -""" -from dateutil.tz import tzfile -from tarfile import TarFile -import os - -__author__ = "Gustavo Niemeyer " -__license__ = "PSF License" - -__all__ = ["setcachesize", "gettz", "rebuild"] - -CACHE = [] -CACHESIZE = 10 - -class tzfile(tzfile): - def __reduce__(self): - return (gettz, (self._filename,)) - -ZONEINFOFILE = None -for entry in os.listdir(os.path.dirname(__file__)): - if entry.startswith("zoneinfo") and ".tar." in entry: - ZONEINFOFILE = os.path.join(os.path.dirname(__file__), entry) - break - -def setcachesize(size): - global CACHESIZE, CACHE - CACHESIZE = size - del CACHE[size:] - -def gettz(name): - tzinfo = None - if ZONEINFOFILE: - for cachedname, tzinfo in CACHE: - if cachedname == name: - break - else: - tf = TarFile.open(ZONEINFOFILE) - try: - zonefile = tf.extractfile(name) - except KeyError: - tzinfo = None - else: - tzinfo = tzfile(zonefile) - tf.close() - CACHE.insert(0, (name, tzinfo)) - del CACHE[CACHESIZE:] - return tzinfo - -def rebuild(filename, tag=None, format="gz"): - import tempfile, shutil - tmpdir = tempfile.mkdtemp() - zonedir = os.path.join(tmpdir, "zoneinfo") - moduledir = os.path.dirname(__file__) - if tag: tag = "-"+tag - targetname = "zoneinfo%s.tar.%s" % (tag, format) - try: - tf = TarFile.open(filename) - for name in tf.getnames(): - if not (name.endswith(".sh") or - name.endswith(".tab") or - name == "leapseconds"): - tf.extract(name, tmpdir) - filepath = os.path.join(tmpdir, name) - os.system("zic -d %s %s" % (zonedir, filepath)) - tf.close() - target = os.path.join(moduledir, targetname) - for entry in os.listdir(moduledir): - if entry.startswith("zoneinfo") and ".tar." in entry: - os.unlink(os.path.join(moduledir, entry)) - tf = TarFile.open(target, "w:%s" % format) - for entry in os.listdir(zonedir): - entrypath = os.path.join(zonedir, entry) - tf.add(entrypath, entry) - tf.close() - finally: - shutil.rmtree(tmpdir) diff --git a/src/libs/python/bongo/external/dateutil/zoneinfo/zoneinfo-2005q.tar.gz b/src/libs/python/bongo/external/dateutil/zoneinfo/zoneinfo-2005q.tar.gz deleted file mode 100644 index 4f4457e439ea04c14b51ba7e6ded5f20f31b24da..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 73514 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 z z6&RfUQ$(df-Oj|rQ#}w_l?7?57t|J(h}`a%TyQy%;CV%O-{{@)Aw`ls_Jr~Rc&!Y` zYi$0})bqB1^7JQ3TovQHyekNOWBeqeCdbdH(|74*0lO=t_=#F}a`dkAm-<6BRj*40UbwYg)N?tl8W{kJc#wL}?B$42I#mklFj3qGFwdB zTXIL?mn36U-@jbKG5T!dQma25DQAzie0UsT8Do&^S;*XNIUTWSLoSAA@;vzYWP&@Z zN!&A4$w?*}$w)XH^E_<4MTKRC$?^&_i4AYE(S<)K{0I%C2ZQ7&ceq8+t}k$pAbaf` zNmvqP-{!((RjG2~e-o1ITac}rxq!Hx8WA$tK|f+Uc>RX3O{I*ZE~H~ZLmfBy&qL_4 zafF>YLWY>RL=V$+NpI3%X5Z%?zP=M6Qr=|GICfXlI|A_@N8qJ|XkQlT^ARYGzxq=k z7lzk-n|4}m+B{==r@V{v6L!0zl=23qA-q+H0))U#x#UgkqG_VM)r}p zBj7D6`MtCH3^a~%za!w`_A$xW3lvEXes4A7D||3_7$TLk5y~^MywOZ!bvz&Q{3l|K z6_&V^4PLNCwCEl4lUGPwQk#dt)Z0y`zqGBw{ybXG;lstIXe=5u0<0`AkGyui`@LOS zM$H_BVj6xV_m-0T{-kcQpvNR*gaHUMfNpB2x2HY5FkpXZw^J_)Xzy~Ns(kK#D>81T zy9BOU$AFlG2r~ZE)OqoDLd&zxT!7{iB~7{2Q*j@T>eK=rE&gn* zA$lch3<{f>%&CC8Bdpi+$v z3L?vQulNA37oj04KIrp|F9yX9*lVU({iYuA=c}#?10w_Z*Q4SqdksEj?&Cg8JZnK_ z9TOQxRYamSV#+KrW+q@Y zdhg;)cSoZu@dQVxyaS&C$`>7OX+9sU2Ltx?+i3~iCql{)T@n)o;XpL}=7rQnk1X`B zKbU1OblUtwhTTX=c5u=N2M+q3yF)n#Z57(=^^B%=-`RIASLnf`>^`xdxUZZy2Q z8>K{fo0*0RAI-#$-MKPtA6o2aUy?S_kuv5(sqoUNzbmH*Vwuow{ZM!!mxKMKLOyL> z&VRg^mfsopJ~=LXV8)gLv4rv}z{$DL2;4kfs)YiM1-q)F_hH4*rvS*6_mb!R>CcV? z1NjFLsNk?e-9e65q`j~G{!b3HQ0os>hj$?H`LcUbTyor!?LFG&sx!*@=56yc`!Y8v zf9s8-OlSTX#z%4UpWHP3Xf>mxyf(rugV++g-JJNR3xNJatpOYdZ#ER|eZd`$+PlD@ zyH_z#AAYNJ2V`{~rmne)rIg_Bq#ZC4bu=Q&mtS8Zul5s4)LgzT?*gGusz?n*T9{42 zj!;Cpn2I+`=G!T;JjcD0He=@J1_j8~cZcGG0U9ki3cht~n9GCbP^w{gR&NA{s}$A9PfPfN&+#)? zv`7Xkf~h0-=d@3Qmj4W_4nHqUgnk-m{b!&-^68i8(<&VGa~$67a|ru0%4p{2{GR{J zP5z6?{u#4?>hoUKF+OJ@hkaU)3V%*z{)K<5yExeDUZ zit>D4OnJ~_2WZu}t(k538q2z$2$>(_1yU)G;dtR$81)9vt8Bp+^Xd}*a5-Wx(ojZ^ zhRW?|ipi8!4NF#5ISm(AAt0Aj+2E=w)M^&lK5Z#)8>0?6wUp^?zeV+)H@$m(cDB@9 zn+U&eV$jNV&V<&{S>LB!v z*~I6l+-z{L*lfUfY1gX-DuJ!!A348&vk>VUm|q*3^`Ld1l*oKcI(wQ)*guc1YALl^CEo05w8oc5>r zIj<+oPb|LaebkT!T6y0dMwW1hUVA;Br-&#`i)|wHmG8Aud98fu4c3X`f*Iof0Mua?UvzW8~2-b z;3~Eme4P{4FYXcqb#9vpMFBCC6Zl0o5aia((|k0T(PX;xT|y~xkW>$@y359feMUbe2D%4t$or0KA4O%eBIL4%{o`B=S~hmt z!F+p7<3)S6=ur-9;Wi0wu9f?)1Nc<8hH^Wu9t`xSc^RYE-J;3QylL_Wr75ZhT8TOf z=kEHcnp27kv1f-M{I%h@)CE#ybwi$G_#`Qx?RHA3Nae6 ztN~Y%$nc$I89+lsD{^?oXz5A_$4@G`VHUb?Q#m!vHkt)C~ z{p)0P>?!bW{{ZSx4X;5$kgZ<9zNNNWAC8DRF%~}@C%!p60?*n?Il!`&mWwtVdnlL& zmB6LU>*T>fYp5+ByMH}hiPpCAq7KBBkiM;tY`*@p7~80WrnW_!GEKn`UpU9!i0~e2;E`>- zbg*@GpdZ~k^suY#Nbua;L#rOw=6i~66nc(Ge3|~SsfhO(!#xL*ZmT!S8)z#Na&8O> zrflt?P?iLBNm$Wu=kq++-oSjzP%QrP*nEc}t8)V_%acPbO=$OZLcI@`HQu>vuPVN$ zqwTh*EWqA?-7KAH&?Af~`G;1DSu??eQCPrpTKu->S3UKsBEnB;juK~12^Oy{H#kS9m)LwujM6A^XazyOqa;8 zu5JH+UBh5zTsnr36PIZ<+|~lrM~>UbrMG#5bjjtx8Wp{=RcoeY#&+lnwQJ$DPy`$C z8d-LuP`YQ{bT*S{f>`^ybcA$5TenndTctzjPpbDAj;+?DS6b(ER|4Pr;IYk!V6}gb z!%G50TZwgsPM5`#@UmXr@OG|~@b^5UC%Nx1v}2wKv}Yzzv~Bk|g0A+Ce2Oe?=8I-#Mnslrs|j!`7Xt)V3b#kGwB_skt_S zD(Nx+QC;LSw~`Fo@BFzlTlp?~AX~17S_ybGzks3-qYRMF+FoHmcW-mlb4p3{xcUe-{F}oz+Hc z3oz%Mze(5A{+7T&vi#LuJV`}%10DF1nOh|bL$&6K-p{TsS#QIi$)dV~f+MgA3(mLn zMwN7Y>=?VuuN&y*1}f_9Q69>x<1pr81FIRa3*@|3jd4JEnTW)ducjo6r_1bVw z1#`-g&*Xsl^ujr0s}iuf~aIkK2-QkKG8lPevWG8Ox@C zw@vJ4mR&6sqci;8IakO@-WklZKIHx_Rp^b%U5;m|XR-C7-H1&+*FIWJ3ubUbY=6kdxQ%*E_NxU!912$}>FG**-T_+r1!7TMNd?y(bQ4H8V;W zFJI6Jp9=}5N|vk$*1kM*fgr|LbkS>s*&KBaN*V?l)G&^jLNsko`MeU+ggI3N$(Fw` znq1^pfya<%a{3B-s>O1E;0*$s6Bod7|3TC8|&-dXE6e@I3 zE!l#%XevZZP@=l`GS3b#VYR2v(noM8xlyl=Z4>ss^%ms{fa-0*B0yh|Yu^Pj2dqxd z0nOE+R}f(F+R5FUYLxz{ibN`BzcBvfq{*C98Tw*zXFx z%9Xz@8DPmH6>h1PC;lDDRko*?LxQX(P52!-PQ7rP%(^4O7?|{;=_~KZ*r-w}GJ*%|w*v5peR;J3n!&K`zTOC@1cBf2CekY{>T&jj zSLmKyX2%7LecdX`iI|Be-cQSvkVvIv4iG=o9n5|Q`n4>*32D}=dx%B+m;eufad(bq z4~3ro8Pd9G25#5P`Xu%s@L9NxKIBk`n-UlT91|Wv!2MJ9zB_Uc5<=2UXVkeG>G^7n zILzs5e(!5xe!MUIuH!M=@!JE#tlg_<&1@j4^q~kPPI7rE_M*Xl#A?JfB8LI|u5Mf0 z-PME{@9sdX?iuXBYCKshm5ZromHy130^^L@bktq>iLi5Ph0II2wbTn?y#d64H^~>Z z22;P8%D6UW#?%V~!j!E!2kDSrOo}-h+F2nF4s=uga5#@;y?ih^n$#4X~~1kmhXb6y!Ya z%*|xLC(78=&F8V!lXPsU^1o;^Cp@olVmRlvWIks%Vj7phmT1MAB<5{_|5DBCFBpm z$L6{~Ssg{e!t+Xs8L_cK)^Ao6Eb&1zdsR5XdKzw=pxJ!IZ>|cRS8z2$GVxqE`kUa3;2$IvUDZ{BBqK4;>)1NUE?&=wzlCW%1 z9AQvN3O&8^-XF`WeQ~Ore@7Yv_~$3L0P@X8WoOvC9x?tOgn_mcb|szQ6gSgCbMIex z?fPA)8$Cb6)b~^wG{@NgC1YcSCZ(sKj#hf3mbscGDW6us4HITo!V?AT)V z^Vl2zu)903-U#h@JdQ86Xj8h)44uuAaW#AUabz(Io>zMiee7VNkasKd5AE=>C6s|z zS5ljIIo;*BE~Kf=yX|QBmcDQHsXPyBSSB=CRF*`%DN$8ihtcBKcpm0EMRp@Jw1cn#+N2)$R(K5ZO1V#vy{WZN}L^FJcFd*ScaHoU4f{5r-8V=S@c6j zxP+&ZWQ3&)CitqpZ;55QqXt*@ccRv^W<;ZbVO8C^TS@)F%B&X8@_&#Ii?GywdcmoF?_$YDScl{W%$I|3w+e(-aZMK*75VPBQNVpyoD6R!J*TYsLt`2 zoc7zQgA@!gN+X)v6q^#Flt*3YRz`gYBZak@4aD?Tm6|#Ygrhawbr9T3H7(odNh&M4 zf%)80o0X-^(0u#q5Y*4uMj7I?+7}kiO%uYAHX&)NK`*YnP9$zYi%PbhN+bReLoQxS zhkDnHMVin;=lrUk0wS3i+k*@+(F~7Cp9dmApJ#h4|Ivoj1lGj$a8F zX>-q|6at{z-O)(YFFEbnh0J6Z?zuVO?sUJs+&Zn#XiJ4KzSCV8VQX(ve@yW#JOFYs z2ZJ}b#emKG2N3n(9EEE6ZWDt3HhA7C46t%>kW&93CoQ(i>Wo}hd(XDAxL97`2Tksc zMdWuNJ#6ny>XiZ5Sc5iAk5AjTm5ab?_e!kN>sx!=b1~A>f0x?;+`&o9wnG~;p#CG> zKQdG3{1yw;r2TwrGjvFp7V~X4D>-ga{5pGm@nLgix_hDG;n_uMXk`4LkUW1h8|2Ri z>?PbSt%;@shSh_WSptO@&*1`isTs7{0Mg@|$6c`o(0}!WbNv>PFY5Y%oKd+5a(-BF zdJss=&K#eN699rdK7PMNV54};nyMO&f&Sn|8MPnUyt=*X-EKeORC{{M_Ig6?O=jC* zW+-+=%Q@Ry;=Nv)Ta6bd1C)!vZ{?52&^t6Gao%I~;iGcQ!6SSXle-V&xM?x%uD$4v9zy{ zjcp)&>-`xNYrYQZwV*8U)1{Q2mi}~uc2@o>!B>kVrfi>80(V4Q9E>j87LtfFJOm>; zeC)$Xzva`_-T_Hk{dgv1%NFA=mJk0(EtyA=8VDD@IkINRfKqOQ$l)udG1{|J@0`CQqqV(?;-L8B-|pzzwIXLfP5n>BHvtF~usYgy`Bx5GwPRn@Ar>js^b>qjr#6^S66wkapLZsQ+# z?Ts0*4OH3yj-=Y{@sZJOdcN|la;ed6D;u3{d~c8K%-SzoI?=K{tZ_@cxa+UNfipd7pX#d>OI-uQJ z4aDE(QF-0+uqE>Byfk_0-XMEYIHkPW?E`13)xhyCVJGnZRtzJc*9_);Jq_uNS%>V+ zSIPQzaBTgQsfP9RZv6Fa+Zp94eijPyJH9`aW*@1f{{>0OGET@q5=Q=h32w+jA$If#KaNHweX^awe)lBTGhYePr!3ay= zbrk>3886;3=L_}Bu_D`!p&gzb{YgMmXcEzdIc*;sJpq zXk1k2$6jsULq&OGQp)CGtK|qotyu9wr91=sY@&qF=A%d@Vl!j*5@}--qFcl*rl`nd zo**PZX(7h$St3KjIVhrn5hNi?3aT@HAS>FBs6R!wSiB}**rqTd7w_rIq_V`nIrVqLqMC*14CNWnTLr#-Vx0&w8+y8qx^L2QA$m+Dco{2hN zWdQ5~H*D`$R!%vrH(I<7FWO&G2;#g5Ob$i(81&(VTCn6Q?IaF=EL+8lR1qa!WDeW+NTsu7KRg1U(ZJY%nRHZa)}@Z`KJnVS{t}4 z3K7y3WS2jf3z#;FElyV;m>F0GqCA)_V%Ihp%$E!V+aF!yV2@zD@KxWp9IhlVz%9Vo zAkC4d2y(ExP<^Vwq`(=#nvl1^eayf%pq{?J4a|Xm2djfPf@k`w&(Ng+X7)uJt_oty z&Sw$41MRPylFtj640sFJ9Sk-c6Z#gBj}iF97jC&dW-1n>s1CFC5q zpJF~2VDDfLVAx-np!C_he)$N2C4pst*}`|Ff(3$Mfomh%B6ZpO;DGIdi-PTepFvcy z{=~gXB?N1OzQX7d0&9fO2Dc^YiUmUedjJ;)J42p=$wB`qa+Rg;qwY!0o4S# z#pbgM_6__DB?fW`W(ikc&LCw)o0gMmq0E`Zr6LZVZN3IK~C-{lr!vmfL z?+prBBu6=%cTz?xSG@ZW<4jqW4XIK*NX2{8}L*NfWHwvL2Fk5pB#+ zis>(X_gMoZ>pI#P)Mx=12RJVT>Jp3;6Bb>!b```zuOu!JE>X%?Ree!)Cn#8NHPx*~ zGpMo{%oI}9Ee=blvUtp=OfBEdLd=vOs$I?+zK4cTrZJeVimF?my@Q@jpt7PcUAa~D zA2guAAd|*R$h#=EW+7fp;Y=I%&3)X!y4M=_z#zYk?NI#Gm!tEEtO0W#HYDoe^w|K5 z1h?>`><8n8V@CBjp~z*I*!1XNqQ-Tk7#{S| z*j5gRnAZojSQye5Qk2O*B+=ssq@O&@z2WNKA=baLZq;T?HziKEdrm#S_Qol+ z_A*Zh_Pm5$_IlyJKR@N7z(etiVFvh5QOKz%kWYpd-53ki9hbWmmBe$Eu$LfOIZqR) zTvlPu^L8N2Z?s{~hp+jZRPN+J6RPD{xt&it-yYCO?WbBjMq2T9 zq_FV3rm(R18kB~_9hMf(E018zXOU_Zrg@(3tNDjR%qk8#uOXc2)^e#8y?qlo`}MJ{oyuldo4j(K$Sf%#;PhWQh(O5Nft1l!Cl z#fpbL_IIaKqVK!L;b?!q<-LVlO*J9+PCxainfF{9v%&F4Q6kC-|*Y%N` zznJQ2n^re6P_F6cPR2^VU+Avy)Y%;tSF_&xNxDg?{sk^R_<)(_!Q3H#T-_~ z<=k5Oh8%}5-s$E%Nk@Y35fhUg0SrsoAq+De(G0Q^BFUD|0Stfmk#z_P!YkDI2z8*G zeyX`_232TmMJDkHL{*%AqORHQF!Y_PDC zF_--r^ZsuBBZ75~qYeK4qmFaUBk)L#qi$@6x*E$b?oFXs?qvD!d@WRXd=EF@-P``? zDYL!fyYGB3-6cpN+}$-DAK|p%yDzv?yH_kh^MTya-K*Pq+>1BC?{0_%?!r1?3ufJO z)LgWGRyKwR+MsewrJpKJwT2VsM*M+)xwGJXSv!D#xfz4HtG7eBlll01cfQSVzPN-N(|6@#IiNajIn`DfyRqpWw3UpVW&XO{I;I#d=S*DQjuf0m?cP(jtqr zl8-)pg>0YZ3XP$rkVBQKnR!{Hor^RN2<4x{651?litlkD#~`BfL+?sMQ&jyfjDhGW z4U=&3m!OA~AOoocrneuV^^bhvIFomM(gbDQW7WM&m$@8xg9C~A0}hW;|LBb3PDbm1|d!)qK27^{Dazz~z)+polz7P`~DP>l5-5O+wu6bgJ zZnS6^Khj=3JaSk~%~xAhJep=SyD`yk`ov?vYL?w)$~Vz>&{S&ZKsnJHz_C{K{BT$W zMS4>ec&McxKCWsY1^r?m6}{e{gd5qVdKXxo=;3Tws`WUqK-Shj!YanWsvV zlbfsb^9x5~ykDh1vh?gC_vpCbO06j;zc1jaN!=TMnv$F52YsHJ>m(ENk2#KTEJ$o( zW}H3Nswpi4dGtvV5tI?HsvA^mC1(137Q_5gs`=@XzuIC^*!-5}*Vq>^ai8ZsHIc|w zUi>iXKS_*7hOsyZhyT(|0k18d=Pj5fB3G#UUakN>*euyR1Hv0%sM}soXO|ny&V{y+!rx5|NEM`c(JEqvniGJdcnunj~h*#z3yEo$X=ZU zeJ+lryELFqe^KH4zezCNSYiP(;UPR9B1@L>_*`iS@Rs2i+90R=a5z|Q8G_N6pYtrr z;{-VHX7z}0Y>$jFKUs~<^Zi-wW<+pSj++sw=RbJ*q64P#z@n0KJA}b(=BABIiMoEU ze!(UZf<{ctMQRvE<@E_;Rg1}m%rG_&6~~#$`x!KsN-kt?nfMudQY~rD*H|2%4@YtE zfH8&}hefa?U=HUxT09dC$6h>XB{oL7{B(%&*2O6xIALX(TuATpK1D@GP<2vS@?}#H znJID|(iUSG^ia8OO^!G;;3?IDAY~GW?fpzb@Zc00tD{5{QA-t1hV+6w(=!0##A$%_O?4$Bu(KI1YOOy1yHAukDO+qqk zeSU*=H?G*XA)bRD)R3nvCi%?OL2lZPnmMtBCb0N^1~bDUfqAPab00L`s)wE8|{Ra&u4FPKzOP-TWPcVwA?_eTcW9+jIrn#u_ z2VZvx)do$nCKVA!Lq%%$Jvbx-FnzmxqwNQ7K0uw?Z_(#X?Fg&;c!4Sr-l69m%3GJ%gjFLM)JwUa-66kbwl7U5Y%EomGgj@M{AE&b)H>@+t6!pqvN?0S zayuLAaV&c&fN-7=^U2J%4$x_8Bxs#m{?f|(K=m+~C?0XJ)?0x*++C6R60)uz-DmwU z+heUV(Q|P`y0kurxzvu2cjkKleMUU})d*v~sxZ90Dw);Uz(%{l@bU<02?iJWq1qMd z;aJ*3JNKQgO`-$2!quFv!ZIg>Lx#3X#hG_tUXyHVJS5!p)@j`ish_XO6os;_f5(LL zwEkyUC(MgVHobck?j*Iq1b)& zwt5r5O<2=-kGedl zkH2qFFTa0}d}ZK{a`EWze{Z$!`(lc{>-%W7%Znw`_S%%|dX?DdTJ$6I5!)UCg=ZZX z#VoteEO!=bU}LlbdjNq$n8%lGm#S z)s`-M)S|BX)iQa4E1ARlN1=JU770ds7jXq$xwyV>Ru%r}D4xgjbnt`gFxjQ?OfMnr zXkT>p91LT4ns^ku3j0aW;cJzA+kOo1 zElUp}KxhZuSrr$o_ht*$iMJO85U&O3xmoz@-D`*GrLQ4(dY58!9?v0m{s`-_(Q^&m zJdX+3q-~4X6d(u@2yoest~t9N^uXGcTRGa2Ll@hbWak^yDLF1Wq;gyrzxqNvxHvjxQEJF=@!c}@fW{VeM#IpVuKOK zC9QGkrHa{~h6%Gq@unVAs~e*~#ny(6VO>k*Y7wT5Za`c)I`-&7rdI!ghv6jqRmvbc z-MoHdyG9qgebaD|n6nw_AxABFJh)9xpt`n=qPn)wQU7qQLjRB}xaKv|duXDQ zEn|IiCWJ4?oX=wm4)j2DN6<=Dm?DGEWQ;PsJi;IgXMnQ)62PbJ#pWSg2j{Vu_T_9E z{`!o$n{bI7!K`Mm!_hSQQ;_>jdeTI$X)}zk_*T>TxQXatj}~QFseG`i5Hr1|Fq?lU zYdK4Q^5ij=?{$jqA){;qAcE;r+>}fLs7zDOU+?QN#p!2|GUfQBfc=;ZZw^no}|^r)bf}8iU1t& zNVw6674WgzXtl10{+3*+=TbuPrl*1aF69q!sa5K9#-e3UbC_wWn=>(99nf#KJjn}E z)NfTu^Rh}%z{d%fXR%Dvsb8mi7lpgZbgNb#Q=~e=N?0PQuKtr?xvnL0A4Z2Olm4)+yvGj zO(NqX%_}i-1tDd;BLPQxD^F39AcJ`o8)|Rj<{NuI1I<2XO$(BZX`+ai1)CRPRMZ8F z--hBI73Snk4dLq`V3eooMM!v)(be}uTtp@96?_{0MkzN@R3+Qoqrr*NFGD0H zk|+?52_4Wejx$SJB%c>|^POd(Tk^#DXFBJC?VH6C@iP|jkF##jEOVlP1IcN@PFwu8 z2@SYzc{mgO99RR?5fOU0=$A1FPEiVr#C$>@oBIC#mLXNNea-J9`3yd(<~StiC@3)! zPAR15z-3Zs0l@pg@jCk?aLqUfY=*yaj8uL3YmD~G-sA~NVkSgTrzDi@yw5&SIt`w@ z*WEF}K-7o6226D*Pm_XKw5J>~St2*KimAm(S|(Tu>~Vxip5W#QXyk2ksAU<3EQR($ zk>4}00oGp_?V8?H-eJVF0VBp}04P5sOhHRd&74tx*Rwi5xd*DA*MPr*0%>p0QdylK zQ&CS-(qqul(lST~&|!^P1pS}@3Q5}g29-WF1*WemJ*o2}S7p0++h>6ufexnQ-zG9k z7%`An;U`Bu7&Ee&5I#9UicL(mwYIUVJg&^?KFQdGKFMiLKFJ#(dTr!PGzNiLXkEKx zUo`@0e>Hq=xJq3#*vhZpU`Iu5x9ZVFdYtk*ZM_z_4Y(#1o4Pq5Hiit~IF|R|I2v7P z*77hJ)^48aY)^GyH=Hlv+TIQ2Tn7)7t|saU16%)D`Ounv?`pj*?D2R3`8)<;b-lJQ zVqCa{1)w;tArT^0ej{khq2MiB*R^%EVC;@XKZg{1bwb0_LVzm?v0bq1{^3hSs%c9i zmF~2YiF9f_BKD-5QG8X$h?M<17rhw`>cJ=+H0M2?bn_+Ch@~Fr5OXRE`;GtpE}Q1 zX_Yn}(YeCuiCmb;M=*aU+ly9?o8pP+AK_HEDfe-D6MhsjpsqB4J#!g+(mOGKfIqAB z(#WdUKi?*UVx{1WUViSY#>cT!a``HZAHUOX0zMv{m-Y%T1co&ILl24HF3(S9`x5UGN}<4RYpG8*c9zq z;JnT@?y-e&d4|&%o2AN#BA%sH_T((D(1FL03~*3L?%?@&8DQMv}P?H66)>-_MegB!JdT6(^&{% zy0b74J`I}xDSHTXezBs_0>%d8t$;?L*(-Gq5H11!s}~s3I`}D6k9fcpsK{!DS0F(; zkGT>82V(k$~U!Ke>KIx_H2h5A`lwIU+Lbgf^SRsGmC8$W9sf$xKwiNIPSw{@O%{; zn7D>fPG}^~Z7z(Brd7;eE2ntH&noPs%2H?AUnuO;oA_ zyBQ|#uMv=#&xe1{@L4;gW6Fye20j!1hpdp^WzQxl64D|a4}7+!HYCeGV;Z3Lp1F%3 z08SJffK`D_f%-b?XaIG5JGHbn_ZC$rh@!6_2ry&Fl~30W+TX<_u5bf23!eu-N0o@l z0Ovq|KhQ_-0SZGGPms$b9iR(t?B>#aS3YO>j)wmI#GN%Q*lviF@MC>mMWiQSd)m!M? zg(CvnKR(9{XyrQrd7wz&ctu-YV>dq}oQ=v?!5 zwdOtz&wVDhrj#VDbCsa`^7{Z&EMOmBjEbN%n>$i=@+`SLUg9|aEoHk-4yAV~iXxF` z7P+kJA|S-!@`+|NuuN=w1M&Sbc0RacVUNRgYUmy1j&mxp7Y2$UL3F8aMhr7>j%YIn z6Tqr+v0(J(8d$M3Pq3=96|p{(kkqiGAFg>YkY~@O;JL?^$Rl#f>BP-AE5p#qZ}EzI zmqq0^Y}3jtc&_uK;+o5b|DENp$t%731t)&hw$Jul&jkG$A2wsq1}o>BGu3m-#+2_O z)R?R-IOU47tnYP~8Q)Lm=)Av-^x-V#PdoHz#ZPM!3|5yQ!7ff{DB!2?SI8K7!||ns zZ>G7OH8rj5>1WgWyq9N>@3@aRQf-Unclb*4118$vciAz&Ki|ihg*Z_Q<~fj~IIUv( zEq;_A|0o)3mJ8Z*ihce%npq~~I7}9L%{c%SRxo<0^wXU|?Rh>CfnWti zP79v1gJl&h*S;FkKsH#B;0;564L3?b{UUj|C@@}jn{Qa;7@}g};^if8;#GJIcpUBl zi^~$DCjd_!08tX3%iP`3n0RlS0{xD@YtXri6RkAfA-6mX`(ZL3ibRgfKlY3l*u|QS zH`)GpF=hQDlY@8cJzmD=gyZ3zW~-m%w!Y(|VW;E26f}O$YVg{`@kYI-$sezzO&f(9 z6Ls%u&$Ei5&04XDp{32EK0H(cGcXe+b(1mIZS;At^Xjtb%%&ohPMI7$(6pa^!762Q zGcHB&hJ0JZ*0uVWE$b00T`};^6`gBH?hpT(Q6Bj(E<5t|0SD(t1BL|SAhI=0+$S8I zAcYC)CqnAUXr+u^5MT)=MN#eY+TeMbR9D~34P0}cBY=^G(>n`N3lNWJI<=6-&<}!c zZ?4a$sp`>Z3sV0?^B_>t%b9rZ5tzcwsyq7@l_Mk#83!UVo08b5*_-nJV`d4q5 zUq`;(G~Ur<5xr=YJSv-Q(3(R_TA@ewBLY=*zRVTQI#{CF>ZO4LLt9R{zy@Sz_A_hg zu}7yqV-I|4GwhyLjc+ww&LPf#uQ9x6WpEUztM@1a1FTR0h7uVXMA`v+S^mTBnnLt2X00o%6JqH!xd$fnX8%SX1 z4Wue88?e`Wgv$FxCxlG9w~KYYjt`oIg^rIzEM_3nKGW>?VV7JU!osqduct<^6E{LE z8cJo~c2nt(Ized;U3Z=cu8utRPSUs(?ld8$oZAcJ$yC)^%YUNl8YE?d?`SfGYP=nO zIhJwRmjT&ip}YhBL}O$i8v3L}WkzQ)i(Yth@@Qgg&#f3r=Mql?Yo3 z2x$`xnYuyV@M8sOcr!Kv=48`#di=V2)C0)*1%%%tAbfcPLe|C^Q}aL6oXXg3(V!lRx8 zb2&Bi@xQFu3(DVqs7$gJ(~e$QcN}nPXj5{jyI>b-E@Ke^Ey=t1c4V)ZXz?yj(tc<% zbou1vWHEZ2O`~v{DV$mUvxm3+>fcYP5uNe1b6Fd37Jn-=y6@$_XoT2{uH+}JmiB{R zJAGL9Ip0v8Xdin`iqIcV-qIZW36b>bJ<*D)g z-(|AuypPF`e_k==Z!mU`9&2#^r9?lSQPFW(O42v`ZuyvN(bpG=VoMe#8cv)SRD zKD|?$a>+LC{5i^wmiFN+S7!p>JiU?F9^=KgGNJ-f`8{h}D!0)5wFd|rQ{olqKe{S% z5s50I4@N*i;TfGD+ga|cCVekGpy0LmZ43}u9sfjbx$UF=Cmfw`*tnNmaBcolvntkG z$O%{7(pOHsKrm_42|%+rV_h=&qTk!+DpvS;TfaYhoSt~?pF9ySDsY%N$uhjvA}5ep zD8u_EK9Ip!?7@0W-%zzG@vbPK=Ae0RTNWGBEvV4jb1TUO)XvaY9dfJvm9z0yhBX>$ zcMF~T+m?%HPC0@ImLTR+Ej9~P#g+G*sLI8?_kHv?hPjy|RKDvQt_FQ6{h}r5E-D;y zSJl|1%KP1EhB`0!Byr-#|JwWHaBiq!r35)3&71c}k7=^Jj7bb)N-(3S+y74yVXKd6 zFPkX*&Zj3`J%joWu5!TxmReg_Q?(L%bDHcbI9d4H4I(1l+`9xIIXVtT`oUKUpedOre#|; z9Jp3<`*S+jIW)YpZoH>rf6~ZF23l@NVSVV_Z1kJpG^efHHum)Hs-3i*HQlS2M`rK< zv=L2Gj&txJbjvy9yyex{>xZKowEKyGG*k%K+hs2rve~s^MLga@2bUg>5SG}Et;jQ! z)PvKH_{PXd%SQ8Q%wr6Jqcu{$+unoiJtS}~eIVVfm0$E!FoLxl-IGk!K{j0>83_m& zB~$+)up4NFZI6aYs_J4oV5&LDnkpnyzav6j=$wZH=&~OWfWVm1B%2Mr~s^}7!b8H0A1?}0zj<*pnnUbBU2H;5;Bjl_O8ZN~U%odQArBoxse^+a7!y70WfXc%ct-G>_hTK#7;o-I-gBL)8aUQ_Ca!>h z+(uF8+AZ)f338u@F?mN9@g@oEm@3gHVGw{RA*(qAm2$-TURrDN)`w za6?tQbMgAqQCe+((^gp2N!r*xZIC!&uEy?>&`&{>fqcof~F;mWGag z5HfXoUVA0_8T|sTW^E>-JFZHw0LiQSa_WK;DRU+86;_l)xS7N*bWZnCd{y08-z)$i zX^sVgGJ~Eh6m_dWwG6T4*0<=%{%xbtV*&^4ky9Usm04Iq|wJ z9%@KPkgKQCYq06ZF|{se4&b3ZHK{iIeXmsHJK$YL35bb~!XT*S;=fY;RE?nrf!R0o zUN-~B!^y0J z?HWInEqRPf>hi1?3vq6Cnj_OH1-nYREr`&!RwQP|+o_s! z(Ragaq}u*7E4f60y4Rle>&5zQ)sb8cozA;&dx?+VZ)fiT-b=KL?_yEbd}$U(KF}RD z$AB&rrxQ}GbvL1xd|%DBdaLI;{;kFYf55`1{Qb+J(fG8{{&H>;a=gPvBbm&!yzQmJVzW7Q`Pm zV&ndFgwy`n>-(=y`mfKhr8v$Ar2Z51;c~{7I$OZ?(^*JDaWg2A zD>wY!Wm?TJ{&D3T3U{EYQL&E@E@VN-)4ltzNWrUuQ(eWK^2S^6FbRJ)Hh}hy%VD7` zDs8l=scs;_QJe6o_;;MuA8lM^48QOL4KQEjfp|%E;|wg@Nttg4bV8FXLi) z*;2DbcIm{R2+39k<`lE2a)Fr8U4gMQ`jjzn!Xx7pAOr!LeGylN!wPr?+xhIgzV{}t zJ**XYJ+^x=9$xLJ(02-g#e^5*#hT_5%0@G_4Nss-bW^i7@im$SN<9kpe+zuKqyNm0 zN0VAU6lpO9elg=DH}(p*W$7z%fPt%2z*}d=eV9M;)a`!yhsF7hr$Pj1 ze2Uy6{RoFG5b4)TS_8n>p)`C?W@#JWMuFo{Y5_m>~gQj5z|F|xf zX(++Xucb>lKyMA^ejo&p(1ort$E8>9@%+v-JJ$o=aEOYF8NWJd7ulQ^2ha{6 zp*j{Q$tt9!5-QQrLG46M4I>;{51-_d%U_vb2~svTkv{_ji4+j+vPQNlIA=9!VW?SI1b+Kute6zn10bEaQ<+thz=I!l+QaOK(-%rJ}pU;(yV zyr3L0<%cx^xOJuu0OB5%ZgEhl8N=^EP~@_QsO2IE+ZSoL^I!6uth?*PXSk6q`XWN=L`%BwP3^9mCi8zwp ze-C}WA0~Znr6!x`kvGi0DeLC;r1#yQ-vn91q|Y}na7l2s3DCosvdM*-;yoN2UoVq^3HKSN62Z z`-}Pd&;FjUU2HiG$yGZy_2bOT7mY`3;tv6DNZ4o52an6pHt~^9NO#B3MK@k`=NiSc z5iO&!#Bs=pNp)eiz#)nB~Dr95#)4CLX8sKKQL%M(MaN^_L zm|+ui9U=O@uhZK0=E)x2WLFsNqZ(wvg%}@~tf` zUA)UsOt`baB^>vwvotUUnS0)}JU%s-8hmkM9}d|B)h9p66bIacxR8dQfq%ouDe4{2 z6>=(tdT&mHta2>q#el188@KgJ;>3LasCtkC;(n_t;?33}YAW$Y zVtyx%VBX}LXEpK~y~}69ncmBl|QS|4%WXM~Uep(g5S_E4BqPZN})nF81WXVUo8A zlA%Xplxiif<#a0*Rn=#ou-teG_HxmN6`6Y~OwSW5+^aA}6R2skcvRsts?N$KmfQ3% zJd?s&G2bJoaqiJZQbD{&bKl1g~fHm#CmgA zO*N?c3CGX0@KNt>^OEP1bxqSIm#(SldTCFg{lv|FGj3!DT~pw%@qhL z1YrJ&tf#JZdAL0yY&okAl44O*d-ygaB5(dw*&Lm%&Y1is5IR%>nj)IsXTsJ9Df3TCfDsaIHJAb?}zX4!2KLF?uU;rgvj8uEC%i2Vr59*8`86PeNJwLbpne~y+D z;nQ7Cf8I^=Nz%>(oi5-L`b++pxJZ|m*pZ(f)+?!M&!dufUry3vsI>U?CG{A%y>DJ~ z`wIApKKM$A4$fdCv>IUzN&ChnEO`>p1tN(hU0_-PN_~1{fJE3@cC9Gkloru z&#mf<$E-(qQOhj8DeoebizPLv6jXZdbdHfLu+N>X0FRB4$POR)&gyfDcysQv_#~jW zT+D4PdEmT%!q_e7)TiZx!Sl)+L+!Pa6^Y_E+J9YZ1lLV#K17FAFNqWqm^poD3`w3E zZS`~VEB+fWANVh!`J$ce7g*3)+A#OY6QzM)-<}4m_(YTq4MRlN#_JU<&K`i)fa0^r zif71wX3!hpPZ}$u8qzTym^cE(I%d@x|<%HSbJ_+0<|dp%R?N1 zoUA`2TNlaKnHVBY#be_`mLp`G;uDALi|_7z0Cp4nNANa88K!L?kUIZs(6{+>0`7KX z@}BBno=9B%$-7Fc;qPh}QJjNE2E8g)9Yocj__9yUD|e9S)kCV#mx1CjqR0je=~_4T z%I|-5ZZ(Lch-m@NU+!tpJJQ=uWdF#th$>-yREiB0+ zzjRjHKij%=`uo?^zvt4`hgXWbE{ODpYLQ!m`M*ILV-5f2`&TO~+8z>n^A6I!`C_NJ z9IXT(Lj;|w274K0E3(x@U?9cNqU-nQmSiwUyr69<#f6MDGEx~7>yLE2Np6F9J_K|uHCAXd{ z#%w7(xDBVK4Me#h*v8kOnbox{=z4rt9KSnFhzC=phbm(hv=YiKk-6T}3rPzpF+6t@ z>;>7DMF2G$OfabOJ$PAE1mv2F-G**Z`Kosc4$His5%0b&Z%&=Tq~0g2#1tfc>ez7; zO#Ia!c%QkxO3|ZrMO^GI=w?a#*KtXZ)6!li5TT`cW{J&FL}9l9STPgc(}Od|W^n*> z(``U3=@w+V1qwqz@N^denKi#|YVPog6g%3l1*~X`Su2 z>z3KT6@PLop#Z8g;iY0-W1!)*Sipe`4nF?|RFtbD>F0A63-%rlc3kfF&d~`aeH8i1 z-0=G3-VcY0au5vh=_=_3iJUYC)Wr|6&tkUGd*PH@xZM8|5|kr`>)D0kOtW7%!{~b{ zE?dfvV+}aaVHa1RrS==JIfJ;YrxQHkYVWe%C*wde8K`PP{&jlB6TPaAB`ZS*AY1?* zLtFNFkt2HU;q|5^Y}smvIACb}!F+h^c|*iasa!mXVl`lE2CO$6D+WDDH*ExuO3fahw;TR+_XK=zkhmHjJgvg;CxdAGVz}&LaCir!zFBlCCzaV5P02!rW zEhmK|ZT20&qZ-ou`y*!JfbHIIq2I`}17K?<0AVcerS9d}6Kw`4Khv;jetWZ*a0hZL zk?3p(Je++FYHo-rtiPC4iD9)OM-xPo*{y9GMd!^a+U#%YI-7)sm_hk3WvF?iFRosUjCc}Ua2)uI9@q!hUg=R_25Z-U`o2B;V+TAy{@sKf^V;fC+v97HA@V?wAE}XY36(?v^Mj+$ z`Pg^d%SX6$36fe;L4x=a7xnM_zL-C^`EaT3@Ow`{tRz^6KHZl2nLe^%)1XYyt+E51t&M=E+<=#3;aNeP zux8uh&D-YsU}Q`}KIY`4GWK_of-`=(m?vT1wxKb7JN3TVQ#AlI8qgnukQoCNRl~rz z$Rz-|1H^F@3LIBr3Wn;E_N8dp{ziUfeW3JLC(v|JC-6E@C7@4LU*aj%jU7c6y`Z$y z6Ms%${IlOsw|ju*6>TJ4{#R0v%7Ig7?ldb~*mUu!sk&h^ull^HBdd+nKUNzRR#J~D zl4w|)VDx|xcJxlehv?amc+#U4{bafrFTJu_Jr=3I_@p#vX{i<+DyeJ25vkJXdmMp{ zDI85+?XQ|jr*w`9DD;lwJy>p=hx8tJq?xJQyz;Hry%8*7P0_4GOfq{EjbiQ zUltNP1`$T|tg2pQ2FPxnh{SBBG^lS%?5jkBr=E|vb-Llz8-5S>mC+;3BV)i79Y}t% zw$FpRq?z&rd|nzpe#nHoYNw4m=EL@Jg0~>{#bO3&ZV;Gs<1&?$swhv<6Olu@3Zh|s zSaea>OxH{P8Kc2bt8JaE)6S-=3i<{~@Qm^jO`FXG`^g%>kf`RRb$J( zEZ%;DTcEkJlAa8fNYv$xk@rnMqwdU-C@~Q)N2d7g5CRCSXs3Le2T0RzV4s`Y8tHAi z0fd&+q+5^_0%J&jPdB!A?QiIP0tG3`#**@Hj=rJEUYosl%p}i~a>D;(O@CPX`dz23 zOIcT+<9nb$%;Cc&w!`mw3ZVcuG-pVM#E16==r8sl7~;>z&{OU!&{}+0|G4i5<)yTc z>^pU0?31Ptd@au+`09Cu-F6U;ZX+dDwn(VbZRRNI(8Z10*fK}ln7tcmLd&aIrRVgQ zE~W{6W+9+~-4Czv!UFGla}~>PS@Zp3d0->ijMrg`xh%wg{$2Z34EkdVqyXU^i~ZEe zB#bDV9-AOjjL?&W5G#*>?oia`xq3`4o~W;1-XwQ9+Uh8^=K(cNOUKi9^R}31&6h`I zk8R8Q>z$E*7#~k`5`^Z_rgoI~|8PJs`Yz832~MfIxhz8b9X~EzyiMCVM_hz`sNoB%WAY#OW?Rfp z5%igl%SInM#wl`sJF{gr_f!59H4NU}yT!r_XZ{1hyE|;ie>}+Ba2A6`4zL3SyP#=)At3N_7Ji0RsE)+m_d&IT?#KPQ2lZ-9 z0CAl&aBx0$**(hl@ZE*Xz6h!*QU+ZeIrLTR*DjDk=zs(RMEVyXm8ZWp0XXmb0U$a+ z2p9_g1v|qnhq=z;zPo_j;^u9FU`nW_eF-ygOAm_)fTR5#InWxTLk=JYQ8y$uDcXdz zC{$+xcPbSy&?wov3y=pfs)4bIEX@^=xQ9o-4N~N0Psv(VJ_Z@8T*6U+dKxK`iM)dW zbdNyiUgFsnX#92Rw#*0sa08tYpy4m{R`uNfJ{asWe>6%`x}^t zxq`8Yq0+!=UCQMgtj4GP0WM+_vhA7!TsEgB*>;0r1TPqfLrlRy4+bN9k0tNv)g#C* zzWNPZMi2$9B7j~L?oDsA?x>(CEIF!j)tm5(f0jkNQ<~{WNS0NDuZ5b8dQM`5>W$)g z%uSOy5!Xi7R)j(na()FW1Hx+pQ5GFELY>b1(bDLVjt`P8@(W8<6}3{GC{O917FNxl z#Xf>0I!l1f&f(yC(hFJlXZ&`inG1|hmGWA9J`M5p;An}e9&d}`xD#OyeB~l>cMo9k zDh~{AG5%WS&Lxx3Qq#^&*WpdczkRt#ogie3J7wuLLy%J!_Tm=NOa0WFaYM+rePLN! z^Pc4m$s>_T@I5S+jClTv;b-Tkf$_&$)!<1I_wI5=)pbHH_dhmVEcfC5TEpXhEME(i zw2TIY!f#fOGN1jei9WxUabPk-GBAY^%_w?G$tvDi>XRXy$e;aF;*M_>jfe`Y)0K2E z#N&Gi$QO0-W#qR}sT1vDpi=vJ#`BE~{_Ixqt=1;5Ka2hNIrUp&=J2D%8n%q-1l3yp zO0{x+7G1{*M<11n=!Q!@p@XBh&)|LD162R46?ZdJKFN)mVPw=PN&aZWdiG^6syYc!zK;;K+*xT>}~|+?ZOCar76e)!zu!GlPGpD-BJ^8%2z4=h)9*G!lnoEiSnhEpZZMB0QxRH>+#*hR|dK11TzB4wX6_ zJUmgtke1tiwh0&_Bd?}?s&44$?|@D{p!K85zTzmzGzru>*m`l7)9l+qaPX}ijwJOu zIw5FBm=RTMlcLSRI6+_1<$XIK7DO+eL@HwRVKS#;a%5PB!UYzoqC1EIujXpFL>CwE z)s`PnD7=JrAnOgbRz66NSvJdIX^-l0Le+5cDpJ3c7~*hjSa!i9e@9r=w;CT;3Nr$P zw-7E4z*!p^hM?I76=W^l8;pUX3`IEr)P*W_dcFk)OXzTXM2!=!@+c@P1t{Bm3Y+3| zUJ0$(mYE&{rvf$I$SE6JJ15}mJ{KWV4lHNGiEpDL8#Co$G|J{SeNUn6Y1f^B~GR&v<8v^vST_^wz zkVn!Upn%ab>UmXy722cU7sE@@wf4@@+;Fgi*Mr~bA|W4O2_LRVytr-_kS*|b6qYhq zML_4ap?N{SxC$=O?YJaFg~idS&CwZOPm`asj)8C_-k84nlL%M1_TGWwGNERoE)u#2 z{Hp>x^i1ngoWAS&^%W~Tu^OluMA8qvwYqApO166BxrS8W9{`SF571B{*#^Vv;;L`K zqxgA`PqE~fJ5}id@-OK*#^1VD-QDjYLAHj5)5!TEIH((_X}DwVD^%qG4!6Oq!`93z zO-#VoOfD*5`RR^j`Hk_%qS7<21^~-gz6q66{QX;A3{OFMzIX$o4G(+#mIHk@!nX7C!KXc>9ykgD^!WV+z zioCqzdXit&LJ072%Rk{AD3=6Br;vjTk`U%y)ZQ=*xQC$j#xqj^uegLulO<7+q>}}M zM$i6+pU#^ydd-h;+_NaL%(nV@9iQ~&Z!=&|;qCz&XfcH${h;@}brJH*C)%#k*RMU# z;y44KySDnQ5W2p*Db9-Inx!cEVR-T3`nK_XP5#&~B38!x4LO*;N~mqGibS`5G3FDu zgRAIrTOQC9FRyKJ=D06(vx|QPs$AV{OpF@heK<`MSu7dA^S^j>^Ax1SJ;KgXo*y+7sU;a(*~G z_jl9&O5=n{wchDddcMZyeIXszu->(_)>3srxNz&|+5zqC`@P!Z)q)uDbOXSCJ($8; zk=Q|{*uFAG;Ulj7ZvRAI<268^%=KDO$|2@YdqPtxr7qx4jkq-l0{B%}wUJ zixnu-#f2mJeYmsLMo0Pbd9(-fI(!@E6Ou@TNneRxSXEkB@s|dzDb=A|%R*nZ<4c!D z{^znaqKctEI_hsk)XUG5VNIXS=len6_f4-F({#I~2SMzm6t`f3xNVT>B9I^FaX#mb z?Wx8FcxO~zePjdupg{}zrH_=Si_&v>)r5`t?NFz+>ipvFQa?@|gxOkizxN2T5N>W< z+uFL2d2q%{gWZh|;=EQIX(C}r}rh=6xHNE=a(d&>}b{X#%f$t1vFST+CyGy5WI z)Lc)~*f@rO7TEF@PdDCAu#2@}bedQuM|9)NqY+8U`%lZtZ1`rL}>!6RQO_k&e8Rto4>7$=s!{HI@r=1dzFy4NhJTp24)%um^0>BaaWG4?7=IxvW z9H}n>)M>yj>d(Y^{1fx5d6iD)xoVLcmz;4I$?&;bJ1|gQtC0Pj;SR3gySTw$wT06H z^!cwz=$2%0&%UvT7AY;Kf_TD*Tu{-m3@#zocnnOHwu{sz-S$B zZa``es=B=pMVPHky_i!FsYJ>I0KbIxBE9p)QeXh;iNW-{f`uC~SlX#IK|FfcFcqJa zOCz%yyE5HXsxKrvsW4z zT=-#}m^W+V%zaEDs-A7UR0Z>c*B~!839)uhaWn)z;%rlK5Y#M&sCnxg70Y%5pza$I zIy3${iJ`t~SSIHhXw-QGA2%X|MnO0U0`WP^;%65+02qI!4#~p-pn~MxEP&=}1LA7; z<;i-OhtcO%6~-eCljSr5No957lt(ck?<_C>oM|KhNylH~c7To=B>Ms2>hXXgV$ZAc zOrzi5j6f{;P5WmP;s4A`X8^bG|GjIJa*~L1HqBI^pPR~E{S^5wHqY1_Zg#HMhNo$! zZvqGFkR_^XR`OIanO%{8J+66PXXVkq==}ED>gLq4d*3Q)>To%CV*e4(W^wK2<8{Tj z-hvi&&u_>{&Hj|vrw);SlxRVs`2l;)T-Zt~O0TdK7*vNe%vuIS z4nh1IXQ-hHk}C-YPfX2)!~f;&K0x(P0OlP_)J7s4USq!Xa;0!@3leE!91zfEd$4Gs zomhX2){HMu4^In1EA~#LFT8#dtF7;cL9P7VLC|kY==#ptM_E|1a+s|IZJX}{O)ao! zTtEzbY5oQ0szP}0fR%UuQ|`N=q2CcIb$6+Ie5G(%;775wGxaaekTNKBo_kZS#(Oex zJ`O+hkkWb-T~w(gZ6euG0K-~bN3fN<5u)j8_o9&Y!|Q*|Ue^1-g{*S<(Kzn{NZH2c zhNBSi@R<0w3raOeiZ!zlxk`{S<4huY6WUP-1eEDrPF+qSzT|QDr4MUKOs09BNXV_- zPqmmC9y) z+(P0VlADa|uOc{Sh6oeyd~50JPQ1)r{Oz%3&R~qKt384IkI?zi#~L$kn)7-^>GLeL zI=PtXZK7y3ew>^^x9i)KzaQxOMg|Rb$8kRWS$>q~9vmQ;_oB3R3C8U^5ykofS+$hD zACmZ!2#wvLgXRWH&dVecYb_HHrer4xFvX8_=r)ga<_M1lz$u3VC8m;LVlGzqQp(oj z@mEyi9vl2Xq*!clC&sIDO#)`SMVwbYM<0|`bIG2I4<$dNu*It#Y#&h}@Fj{!;7?aE zG7Du|b==Wu)BgN-m23NN8e~klD{;(<=;X(M1!VL^qhGnxbq^x8r8l~ckIRsEQ=lu7 zv@2sgSmBbvizz)xl$TEZ(uQAjkRWPVMs2dw7PGkFE~PYkCPMdYY1Fs9tgd0}>E(6A z5(9x~HoZO2~TA(mdmY&7YY?E9UKPEwRZ(YbAnIU%XOU* z!RX(#wp=nU)4)ZetzQW27s4KVIdT?*wDe0-bzaAT0yf~bbug#!9H!w1I2^qk>%0WgVm!< z=2bhwGUw$8vW9vVn7D!ricAv58nS<+?GnJ&!b9-B+Ya^IUig&5H1T_qvFih6Y5Gt5 zns%f4$sHq+oD`$xA=aOZac_+7Jaot5^I|^-)&lc(1}E}n>vr;P2nddoL~x5PBTzO5 zEPvQUNso6nuy5(sF>Y%cupX&1LYn-ff1c>sRzksnyzThre2;w(MgXnQLhx*JZO9dm zRTy4^6;VOwi~hxMvG~>})8P`b>{C*C9{2KzE;pF_@?>G$HTbe3F|eP@`ojB2PHdWv zDeRYDSf5@QWd-}5QpFPH97dorzi7T>|Bn_Cr+Ig7Qsa?U=nuE85inTC)5s&lH$Hy^ zI{z8;3FcR#VDwV;o^G0o?z{<^&>r?%E5>@8;Cvdx4<9o!7#s(!o%)AeYu%(NW88ME zl6o{>|1y0=A=OP36GZfpucWW%Csg8Nt@dL`p_f#TvL)}6ts>eUK8asF_n|`{df|Eu z*>338+8tr6M2}RO)E*dH*G3p$q(rPnQge*HlYc9I|E5>h5}(h)p8JQF5UGZx4D}X0 z6~2wCWbJt4vpj}I>~EUIVOAZRJ8A1!c4gT8WX`AE7S&7<$)fY`2ZqE!Hcqt^@ zxo{*~^J%;siiv&vss+mKONdnuV}S2nf9C$e3DJJMFV5XZbx>6Gan45 z(5=YBBz~#rI~tZ;>bN}pIa*p9V8&cLQq@zsA*0v*thkO$XbZ^yXL1X$!T#%&DPYtk zn}$b8PsjyGf&=~V`Ptx_t}f&?z&i!yAE&|9{?wHQqtD2kn16>LR*5>DDL`V|1CFVv zi7i7)y{D}4*S)bxduR)Rd|CLrog{LZo~~ryG8vy(R3uyIx1FG*oxjWvg)Vl22?1-d zlKxG>k3wV7Vo$t@WCjYBO*Xg%sm2=3rA}?!Uf!-S zzbD8EB%x~()a@au{MLy#RM*j0(kq>f^P@(A^LS6gmQVy2Rdvu_`%`GdCwLOyKsG~$ z&Swjct89UeSo*^3S=|B-@v4v7GvQO|$;?_6zR*=VqCFrl>cdJHa~S?}otb}b*Jw?j zSbmeo^G(JO485;_GFPaeYbXSQ#tE>bARdW)xbh z`ywN=hg!k`(i6EZYsKHtlGX0BE;A7l{4V zS!bO!(u?fT*0pb^BApcb$<;+z1X;9Y-?fPQ6$pY@Sct_O_+RkQzDP)&iw#ep z5=gZAe0uJV8&Y;?&|P!xv6$BHJE#wrpLw>)rwh-_SL4oIyiRZhXe3%BcIZApXL+6PFs9}f0NI`3wpPf)5jC z@BGV8mu0Y9ep+|+!(e#Uz~_~dUCds@tVO_il0bhVmr~2nkH@Gzy65!brXd8TnNb2y zmPu-QF%~48q&NFM_H_}7MS*LGkMDe>dS3nv7MKduc=E!31AQ5$xZY{u^QZ1W^<3$* zNq{(M+t^d07<@S+e9w(%bVRB?w_2jTb@$e_>l9&s7rjC?ReayJdBvNrf3SalwvQqhux0iQ{6w3bU@33qBou-5 z=NR4UUd5l4iXrry^w*RlNh!jV=;&jZgxe}{j}Q^s>*_R8;?y!JH+z}xo#=wM1jC}6 zZBRlhi)y}18k!(NR@6-jck2p$j)j@L)CxQ9^}m2X+%ql2`Q zatKOa#{bwSvg6y+u_K-+k^7Km3fJFm6&p+C4blWOB%EsJQzDPpq!Vn5t%Fo4DoqL)_`q49Y#F z(D&2wHjL914asCmW|aGh6oPAYl3*lmyj`;`?9GKLsqNPc1cIkOlqfME#A0^as>!&! zdHH-syT4o+eam7ZDN|>EAGc_u{%bxR=)&C#?P##IHTKOV0 z8kI*f8`^Jr2?RCe;URcu8#A1*x8Z7;ihf!ZaYJwF1jw(=u;Si~fTgL&#lMR)>xGCC zm&xS3SNfP8t}LB{vqL*Ur7@KrsQI1bL-3)Z0_IiC2b8bcL=v+pCH{c9bUVH}QfAZORvuKRRd?@8%6hrl?u&>1HVjau5CzeOI3Sn5(-i{zu@2a+1MC zXllWO!OR{MSyc`%jwSnm??tqt{wxys2}nda404Yg?E2$+n0wao{bL|2=GaNJV8Jdz z9V_PczZk^K-ix;$E8Rg@XUN+n&KOPlTKG-^CkPL^1K+!4UCai5#)b~07|gm)uv#3R zVtOoUgqor67U^y&{S$GZKkmhU+kqQalyiap@8W6#!QBkCTeJ}wofDVtRf8(Rs|%qw zk|V^wyGcGK{Ae@c^&kli?TeMs#}c8X6BV7hJY|pz{fx)Uef;i?ercG@OmxB_s63eT zR;bkM3G~d$W;Hi&%eTuc15Oid;M8;MEVU{g-nu$3?1c8 zF=Ogcq{~JAAEpMeAIV9t!b9t%$*V=`e9`ziLJZd*-h6{=S+X?-%PAsQ=}m+mJS`^b=t^3Dgcbog%@ zQ7#ZFS7+vmw(EQZwRWdkAj@CCvZuSTlq2N0l1;abgEBI*fy-IH_8mT=b4Xllu@r2EarlZ2YMeo{~hv zYX|Vf1BR1=0xSk1q=p@`G(T19|LORHDa6Fr&^1>yt-D6j9WJ%CvDM)HSpAKKNsCy# z&`uT%Uv*hvso5J)<%*s274ghd&VBYCxYet-5$R`9qA8KM`!daiq!vf+u#8FmhGpvh z5p(o#I)h}V}z373??y5 z9LqfE-!<^opS+7iw8Nt{s2T76b{E*x@W5)A>pB==&u%>KygI;77&~yJ689chi2{HK z0T|E-=}a_?C}(y+E4%Nlo~vEx$j8eT*)`*P*+=3~vZzW;&a6}emGz)zew+-J)~u3T z=~a(_5@M$x#0VDGQI*|pRSF$69#IK@7 z7%d@v9mM9E5Cb86{Gw~wn?j6$-OV&w>cVtP^`X~akU-ruqWM>j^Q+^BN6taYUuc{Y zT&*l%r?$XOpPD9IBAaIn_+2yvo!%*(>GJVIq(6KChe%IM+#dzZ&N zHE2Mo_jwZ$_%1kA$Rt z%e=g z9%Vy*`@I1nVd+b7_3c8gS{2eD_X`qX7#s2@&&n#wMn@Z!K5Zojej1|G(*ZYox5+DM zmMV*oR&$ZFZdIC!D) z7%096^G?H@d7v9F;Q(xU&rgZ#NXzdi>LSN-pfE#muI&QLq>Mm)WKwx?uZ3rt#SNRa za*@5g#hgw@y+-0`;8vg1gjdBkHo|P-#xh^vRZsQsBkeTkEVDXel=Qe zknN&ZG(G$8dp(vKG}`gQ^+Y(3ip8~Jk0x#t$b$+wLzJJ3!$Yy{dtrs0QrD=>QPpj$i_O!Dh)p4>K`NSD)21#CAV|IC?TV`*1eH)7`egk+b~)ox}w!=q*R z{BE-0JbQNhHOd*YisISD`)@Cb4GL(PBLk?!V@bHnxR$t(B@LTwf&E|HsR|>Q3o=XikH|gqUX>|jU#H_3};7@(<$1k19xQ4<6 zR_Md|CSequ7gcYH@eV{r7bKbQZpONx)H!+FXn|PV6K8)>NpHG?&Z3KgAEdf-Kd zlgw28XhWHvH15{ZTNPxo-+Jp1DqF@YiuUgvYlIwK9<@2V!@#u7lMn0DME5M|Nrc&` zxuTuJnvl!vvGDO>RF;_$tqPT94SLK0<=+p4d*58-!ji=t5mN-!*p(H?B3xs16`Du& zV*D!A(k(=g6@}7R_l^wB+mvC`bs2YhPg3kZE31%s+fO2oe=i_ofM5{{{da6O6(%=04r$hznCa7 zzyuFkrq2mB+K&6hUe$p~CZP$9quahT#d}NZ+@&E;8YhltR$@AR7k-8d3Y{_@qjI|3E|%KEsj!qip&$ zEc(DgXeOFXUKM~t_?07$yzVrJ_3@7$d19jiaf^L2FRN2jO#S3X;T}3^;%rN{*edKF z(lSWXgUhfQno$jHejiew7%Zp?9pXT`WFt%eJna(N=(s(h#|ws4OBqp>kdi z?Hu=go%Zy<;6f{TCe6Q;mT55~N(pWkV?Xm3QROsMqO3$;U>fTMHu}qKmbOnltlulE z3axiPNv3hrmAqXhlCp-Qk9!{BVxVB!r5v%y5jMm!mt1@paTTE-EB;DLkTLXkJCUVk zHNAQIO8VJ~VEEH?8+5NGvTte{`Y2!Xtt`CcaZH^>^52!51s}?k?#y zf>xkzX|x@$X1m24Wwyevetv(dQtTKU4V+`jFd22o^>|YHBh84)1aY-AyiB0h9t*}| z_$mSFOaTM*W8H6dW}b&;q22}aM6!~1Cu>Dr7P@kZ7AkYZ-Vq-CEpnbq9%<|7Jhmd0 zPTT>9`C8z8^fW|0cD_ZAH{91s4MfIhIP@86jK$FNM^Y) zsEgdF6)8Hrnd5znfBauwi}e9k+EP{i8qSl{{<646B4A1b1W%*~l>KQLR@1reH2+&9 zLpO(B(qUg>;g57%S$uHQU* zkTjysn3O18$XZ}I|PT+sDS2PPA5L+bh(u(PT=xQ!d)jhsK3>Wfs0y9LNbDS@rO zts=D1@hp0TnagGVs2_+QiDh34kJ-!ErShVQP~xYw3{vEmq$+f_@)*v-@!TJ?@Q9?s zN<)1cQ*n;3CeZ2#?3ET{zc#+c}S4^#t160V*=iCHiP?M4qe24-w359KaY(*b{YYJrJ(HM z;9-MQcyv<5s5U5Z%X*2xt#z-*rE)G9m4?p}yZM@7m)1SS?_3BIr>|u=f^7|f3z^TY zvZt5(qH4sAW?VR}ve%?J=fWdxV+Rd;zpMWB%_^BpP&G2cD**p@&=0Y0OWNVVd`X}u11q3yRpgb}zv6$b#dYxhx zThJzp`1WI_h`!`D^%(U3N<32&E&P_CR)_HIqsj2nOvCUSw3Lw~jr}3PetI`{9>e~2 zhM$Ftamo3aP?_-=@6;=P*InC2R;wc{2Chd;0Y*B8$OI;y2;`|iY%4D|-ub>1T0$)e zePbNbKb%7EOD^apTv<~0puagtGJX#Jqi@`6=F?bu{Ihe7@p)tL`dVO!f!N&&a#g#B z5*;zp!3h&MMQJIzf3>FVe)4c-T2FMTJ3n~&@MJ+GK>+9%zjFGZT805*S!r*8_X(wa z2HV2jekTAa|9I>=2>@9Hjwsc?J zaPHUoH(aMSkz8TTTdQI&pJ}VUxG212HsyjO6+!VFSl^|9N(a4}(*^AHWqq z=$Zk0IO+`SHIU)n8F}D5*H^>yQOmO$j);G3!FL56Pb2pbX3%!Y15_P-4T*FM!tSBgyIbo`d@zxihlx(40Zuwp&9sGfDhwO%G~o5_Y7LAg4Vcq;oOlv}>bgNK z??s0AiB`n7Fi>n0k`@ZLz^?@~le3buSIE&MfNki&96X3olJ;NW&P|QmSE%YD&UxNl z&S4zeI2fLNW_=6H;~fEW`V+x?#v&b!9k3;q=uP)hoY#9Dm(CYJIy4O27}voU4sGoa zboP(1f)`R*!$0){{v(60c&WpuOJq#2U=irt(rxeAb2uIypoKH*XTbjPu~artM+BRA z7I_BMfaEpToD+LTqIEwA&ozm;0$LO;5d^lwX_^)VgNWUKo6%VL9U! zyAV})g@Ly+G^b|5`u0scMB+v~T{(cqRLJpDasczj8ORpBOiE7VxpUwFwdus}q&^ZJ z;WVV#%z~*6!+t0m=^9K9J1r<1|4Dz6<~Nu$ZOo~QgrjCFNuqKj{jZE{T?`0*OFjg~ zx$g7<2K2koo8_iF#3SBNTaTj6;Fj45r|{iZNB_oa&yMTOyKIhOfm;(YXx`k<>l_cP5U_81rN8B8 zx66Xkkr9C$=>ETyfTZEi8G;NA6>L!}{O*sDXPQ$@RAetIr%Ea@D?}Dww-2^DT>Fqp zR8P-W1kO_{P`mw<+D0Bk+bP-}BrmQ5NK{Z$3BxWGRT>RD!3V@VKGk_E!K-K|ceukZvvsirt6%!ZUuO6HA+9R6veO0y% zy~Nc904ckTnrJWZ6L7A|#KI$Z3?8~yS;}9bfL40U#y7j&yV&}KG43CSV*^o%hrd7t z5s)2eC$5FBaj=D(F-s?8kNZkWO^TZ@orN%gH0#fe!k(<<$Imro$6GtoB4@)R=b6oF zz7N*&`gij=iWgJ~DQg?1+kn|f2dPE#EUb<#@{^}qEM97!JiQfyr5i`;3bGjIsJ+{i z(3;OGv2^R%1Evj-r89`YU&yD#@tz} zkJe;OR-bGKj>>9OnpN_%eClf0s|)fA5RHdhTEsZc<}dWq(v(Sna~zuJ=6N3QO@yi? zYsF89VKzjp|G0|-HgEW!Kc#v!Cq3XvenLKyGNY(m)ivVNoKIL(;vx|J3(B?{yUpM!CNp0OD9t3BK07oWK5D-U+JSHn-D7;WmK z7|iqFH~!oHp9lY}s7eW(mta$PLow|8w}XJ#XF-B2y*15gs zR3x+%PA3SV`?FY>ovD89GIfY0Cg@7RofETE+!vWM?wiO7prY+hWmhrG>wZA>@JZy| zH=r-vsAVvkFl9*+@g&aID=${^dDKdN@-?8p7`^yYSK2mwTiz-SrPCB5b5DulUMO%w zwq%I9EotGkt{{e5)3Mc#W6K@+5!;1fC5+&O6aAs<6_IKqN=Dt)v@u7BbryG<* zSoK4uhSq-hpQsN`>pE#5eM2oy`@p@8sVL!P=QZD!i?&PKbFcF2%8r1Ah15!&;|I-~ zpHJ;=N_-rSnVMRbAr+mP`6%K1hrZh8Bs!L^BpZ!h{y^m9uMaO1gy=O*80$Hr_ZJt3 z;B?E8M?CUoc=8uazy@9-ebwKrowTldyjH2dF>j<2?_1>&r};nYc>U^ z%QQ8e@i+Bnbu z)*u`Vu2L~^T>chbQ*+k%Fp<-7^hMxf?w4z6c-DSK&{i^pAvDR(+TtCoGt#C*>djuY~t9m-m+ zj+J$;d+kRX3-=gG#pm|vy7u-oYL14D?7AUJq)CkJI}j>OD#`O~DoBAkL*PluG;lTibeT(p3Ho?M zXHT%dvDGX3VBi(ldi)O|digJx_FQIQrG0buKuMBF+CjdaW0{JNUPkJZuMXdZlC;UP z_Fv(EQqg#@)d!H}_;A~3{i_Z$2Xks}Ph{{Zqis3oQe+O_V}`+$i)LVhP$B<7ceU`r z-lajc{DIxoUSNImLyk^7&{yTvQ@l}nqqDTHyxP_(46czh=`PEkGw3ojkXLoX|pe;7NVqF0p3 zZNPZBnApEbTp}+9WZd2CrhvY*GGNjg+WAu%W_r7&I(@;yva8796&@8;*fp}(uEzpC z?AJqa4AY5^w=SzbL@bZ}F}yGzJ~HojYFWnNo$_MSoO!h~)IF(MpF~RWFcHkZ#$oA@ zt&ZlnbTaUR=#!+O0DO!kWwr8UkH_nF9qDW|l&n>0uO{bXJ6T_{Q_Wht^N9C`FYQX6 zpHatwlCBL6XL$7mnymg%9#zcC;%B+Q^AxCrD1oq`JMoUL4!#E?k~w9#+Tr)f5LZH_ zPz7V|y?l=A&jT$ry94aqH9LtXRm-@|g@UO8_@D{F@}}3?F{Xc=w4o{hH3!l@6MNeJ zR_djLv`-)M<1s3J=^ra>=)kQFu?a7BN58wJ@x%qj20G`LHLLqBXHRZTttGoHZ9G%v z^SajH4I#~nCchg_llJ;gS1$JkkLukX^OnKLfr&rHeaTZ$K@`M2zUQy87jAc$Iw&c2 z@Ymg^2~(73ecE)FF{PcK*xOV$t)xw7`kKncbj=ML2^x5;m`|CByYIbE311Z5wh#}( zYf*_~!_gcs2HXZ8LVx>&vkoNdWqv5L7UGK79N#L`^Dtyt4f|l(+)z5O#;U@&>SWTp zX6mSeEq=`+zKdCKx%jO&+b=ZgcP(B|Z;A{v>9@Hm-7UQo??tV}2!ifwPr0xc|D&?4 z55b(Z-U7t6AIR_~eY!CjZw8Q@jm%MqR(84>vRR~vDYkx7a4_}suMOz)+%w8lt@Lg| zX$bLS{9LSDg5O2?lE+bZ>BJy6^l%kG%1HgGDW3_gV-vsh=5c(j-R2+d9ymQ6i^|Ck zV_>xz_~xT7D6%1wK)M;l(09Tt=(XlraiA>i#y}$2SE7}0J&3Im@RIkE3vd^7TicpAyz_mIu zM9P1OWFlZv9S)$-@ffNZPvleTzZsdq!FbK^5R7I0QZ_mN6ww%W4uY|ycbuHybt{DA zb+=7wr;!lOwM}iRQLn&0>oLY+>+r06t7Cx;!Ez>4rYB(=Q%lWZ@S;2V6L2HZ3uq@6 zW_q#u_aWnC>O;HSF@pxEJuhcDrr&mwia}278ng{YPJ2h2I<_zfyAXqVe>efdGc8)+ zKD-jU%#e|LTl^7bq3b`}K7($}bgWs#nza@~0@^GG6boevHoEn~rBQl3btQKH%elC# zE}=owApq%&mOK^TGgw(-Xw(<`f2e6@Y_l6#M^9nx%=16t7PdDWe%pWF=2S+d^rPDS z7HhtuBc2q5*WAPGyR*qzDo1F-3USPs9s#^YdTualcLD_Hji9^QlGNp=2L}j(H-VM+GBLrgcdmd z`kZ3H#+y`;vIvfa@{bj(05N;zuQ;Q%OB^Hh#5gEd7SfJ1?I7Q}%cE}xk{ewvjS<-f zK4fP6b;l5;-0JZx$^VbkiebHFaQceOZ|b*}PC#Y*34;ChIUtGHnCW-Y993(?A9q}h zjOL5Utl}mSL@?O*eoLj#{(M1#&m1M|7N{T#XOT6zgEU*c$KinhI( z(~9*Pow(lUJal7k3=+~fk>q#1b+bk%s>Jj5tqRRjaqS20i9>kv*pEDyw~Pb1ES>hK z2dE<9l=ro=;KgY!k2&Xib4_aDDj&4d-YeoK{G*LU%Sp7SMOvUfK9u87_U`Rj9{tLl ztMb0>_Nn5X<@q76<z{op-ZpKjYJc*(GEk33BS|I#v4 z&Yv78%_vbn=k@*&l}uXl``Ig)+u1*RO(m-{Dd?xuv7SJ687207iY_~DdUmQQz(hF{ z_qbTdK~s@a{S+0j@6a4LFXDGPp@pW6KSuIR+vO}Tt?XPzq376*Mpx3=oJYnSu~N|+ z33SPH3ud)-F+lkH>HSj<1nm%>^=rdsHF^A&7XrDB0OR4eV-GWX zPc-lRZ02K~|CrL~nb!WE5DTd2{8(G73TW)Yw}ldVv?@+?Nc*%&#!z}9v(7_I1B-><1?9wN{$8` z**~RCGRO8$ZPw2(zhhvajY0HV&?F!MAibE@--1izQ~&Z^zCmOcfSptW>hws@ou3GR z7gckpcVMBdmv_L^`vBxywYm9^r$0p0+ZNe#xZ%!`CrD`c1oKZ()@4BDFp^M%ArxTv zSvs-^pdW+(Z3AOWXTaG6h2Fag%-w*W4W)tv@N%q z{OR}9-JFm}8$luT2Xx9z)^Z$Yspmz7!GDo#DM)QYozO9bPoObEDPmP^`QE=*v}6E# zz-GwY-6$#k&h?&epUb=8WMCR=bVo&HHr}+^fZLp^Bu8-JT_m_O#N;FC`^VA1xX|G0 z#D9@hbj76#f6jEr5>{P8h4RO=WrsH~-H-Fyakup}Z~kJjP}28+^X|F-!G-Ah~ z-CUR|>s3x{1y9}LL2$rTg|gmn>P6c#%XPj}PuWNzPUn0Dm47zbO?dm6f5UfIVOJCU zc#rbWy_g4x9E;nJG>~RU1MhDe*yzo9$W;ZqrI$KSOU#n=*dezy318mGFXP#m{|9C( z!r0xz8I<3WCP>rJ*yQpW&L% z20ceLK5tY~%Q@X!PC?cxsreZ~#{9FRN8JCZ+F%rpe0!hCZvS@6V+nv0B1~`s70!)= zi#3eHMWo?6d%xg1{|y}A-}J-n&+Ed?HoVbEEl-dWH1^WhfO%{lEQ;;KE+h>sb9OQ* zV!UEQudo1AGo%h(0>$$vU*Z|(hNquQ3s7M>Kb|%MV1?73PT~hse&wJMV@rK0*^C9- z2%&j8^?MwA4O#ZtvGI93!Q*9t1=(9TYEur*ouR<|)G6<*$7zrAqBmaW`62-{+q&}h zho$K=-~9_4c!8!UK+gRwkP90)Ddj^un6G!?|KvV&_0MNAl2pUZZI|fq%VXPavKyX< zL5;Fo!I17GS*y0&{T2I&&=cPfB-&Juy$m|)@4AwaqUb(m{rn|#kK|pqzt6te$j{)d z{Zaxy16bvW;=U;znq(&*c9Id+DWdUnYwd1-CGO$=!~i|`R18&2wJ_OKvb#{XIWkd+ z3sOj2@N3n_`ZL=M%2{&L>8Bx^!r53ag|+`q{el=qnMRlR1b}71>!JQf&ZO?#IkNUq zaPyyAD929)lzs5`FkmbS0!ZFS!BRKvQzk&yKeyH{#$G90o=>2l(Syq&GuywQ<^M>W z&6zUXRSE+9%M&@3SD!%dm_mwyU3x^la~N9wXf1%W3&1B4@Tsss;K{b?y%fw4WMo!N z?bq9O3R++tF?~q&7eA66AsIH_=dcV6>9D&F^9h3@)NNm>M7Cx!CgVYTJ{)$}3l8n9 zny7V0&9ee8RT0+U+u(duw<#ln4zpsEsr}`|vy|ny&-=pn`6(@58WfMGYT51t*nc9I z)k#gt*3#qbccAi(PAQP%%d72hB_w`8LX%MT^ZdzsvW#N4PTRBkN5`?~bG?N7NT+E{ zU5nAk>XV+bVg{AKsN;f3W1XSzBKDX1U_qKTgl+#z)3@Vj8z`>gkMvjvAs1CH;*V}{ zqSJiK|B~=O0@qdiEB8OhruyrH5bqE@8faq+oB}Ya#RaujdLWwuY?*klq_CTArvIev zxBOl5-~$qgOuTFixK+9DH{Ek^g~lL38Y5e{QETFHv`%L@;SMJ@?BE3be*_%2{_zW3 zE`xR)O%*OHO1bUt5(~HV5pI8dsOM>O17avh15Jmsz;*DubcPGlzy+Zc@GAgVuHu6K zUCsqQ;9VhZXIhq0Wu_h5qoX4ubzXYg7-nUAD(BkHQ`bK9u5YLdaRRHMruI zrZ`yv{OQ`<(;K+E=8w!xN}82+Dr8UF3`M*eY=nzp);^MHdziK zDF;t1n?q>i4V?!#8du30MtvIriWEP|O2eBL){t|w_l7t29k_c3W8pMLis97R@a9za zvXb%cbX(tu*4_?97@Oxb9!t&DoOI1GcvnAe2kUfh)w989>&y6lWTd2D;@GntGAKF@ zncF*}eJ-{%wJi7B=hTWfF)QzM#UZS6y+ct~BaYUPXC<_@UL*goTT1xdVKtFdZOi?gBqnX7+6{=t_E zeN&i;B}Te!&AW>*gTG7{JvE~$#6ExVb^bjXCI{!4{X9*VHl`!Rbvi>s0p{PF-)#yM zJ*-E#ZIn~^brA^(gx4lu)U5endJ(0`K7H}1uifRk$?t(R7{bj}R6%zlr(mbcb z#MG9(W6O{Hz|~f)P4;sE%_CAO;*N6@kREQIQ>w)u&;t6}2c|{ApsHU6PNB8m_9gTo z(OaiMT8z)9*n$U@l9RK_SC@5kV!1~nh5BF0it)xW#kz%0sL-^gs?9hGKY{8wxwatI z7^M}zepTI=khn85jTZmqEKKZ?P#}B+e^nC{r%#&giy*68=5-zbYk!Y`=?->5;ZGE} z9rLBL<`$ek{Va|)N5!!7tt?iE?*Zcy~n0Nxw ze+Mv!zz>iK9;1Wt)6+s_63&x^_VoObecE%L+;bFK8meLz9qa+SLcJgJ9zVqeP`B1h;r*7+4%T+{pXNq`k77Vx~ zI>EKlug^;pN}JXU838nv{u$+0XFzhP=red$0*1u|tUUmuE?Fy8FQ5!3n=)CI!<X zQqK#-7Lr*!OY7yR&PKFBF!e6@O57UEQFlwIlHon!n4px&@eeIPgp0x-oK*1(YmL}PB43$sPVXnsOAJENk~lr6x0CPB5382 zFVBL~LbI6Rb7OP(lJ7rr{T7FS$n*fG@GT15sfBH#VerNVmshS5qwUT!OMfz zGqHd!v3@-s@Er#%4WxpgB(gAY_{r*DjX|(l^jPU><>pg8_|3~_OkWH=*quKFl*JS+ zw{yTVJOTWS1DMl(=1Rb#=>UORL7;K2#mb!^`fkwjWcqXK-MOdP!#9mFA7f&LU&1!I zNX*5!MP1WzUzlF`XqX$DV=-C+BHEv#r=`6}^Il`tBg?&lNLF0BS?Qu95)pN9@G zo#VOQ@TnMDU+S@wfr(rw>zz$n#AwdOyGA{6sX##e@PcE`>rOQ$1PBm+opS=bj(_1m zx&w3kg?{n<5DZ1ThVPO|*nm<+UX1que)Fm&jA9X>OMe7x7Mia}PA2Tq$A$E*W9!`- zKWSQLjZvAp=iG@%W+@AK@{7-va#co|2G#n8{Vn()Lb3pZR7LC_}yP)Y}GO^j+oHKkY3wINmvTI|;rjTwO(=a!v?7I6l0BA;P5u8JFUf*{fK)vhV zi~!P&7DTa5YNwu60;ONI)h%T_U>OOAkt;;0+B*j)F^leZGE2VX=BWv+wRgP^j1HB_Hz5U)&W_rQD$8O{Di2k95ZKdWY}R~=KEtgm@I&?6Cb z%=0%p6XdusEC5=K<^w1gr3wh$?eu%m@jvv5Sqf<(No)+GN98OX<+c+4@vyzSI`1OmB$kEWeGtcKu5KoCdZ!ZGS4P{K4IaE$97 z+9o&4|Y^-K=7Mij!fO z9rp4LEaWS;Xo2=Do$b{5{dlTcp4?-lGDCqK1QK)oSgA!z{gPuR zwAl&gdSy|x30z^x6{<<~ARrnZ+w8;VzV!F&Q(&!Yo77dsy{i668#Eyr|0 zd~t^Qy%qY9v$dv5@JcbnTD^Cs^gf^h!hdpqG-o! z(*S;q2tPL!bCSKs9~);sQ@MyTEL}35Ik?T;t8SeYstnCWiEUi=l z9u*5-azA^o{-&Zki?}oGpOX_TEUumk=Nn+KPY37qXZI>>_VjI7!~{J%h2f=2miH0Q z&RMoP{DCv=G(`bA1|$+|n5_PlX&UA!fNu&FFr(s064KQmeIlbY#$eknDF%Aw81S}b z1>;{`nx0hyScp%iyjM8}74GeO3+es*YB;mJi@Ckzd}kH5b?CjXNS64FJx&~r3r}Y~ z)_nT+_diyibq`_CfK+(;v7^{c{voovUV$(two9Rf&?g#cvKvzgsIDBWbpc@Y$~vh~ zShO|q^QXPfa(&eTHmY$1uC@;21O7OGoQ87oJf|%p#K~l6){N0a<_~ZK{2?@pkvfHk ze&nE)_HSf|FS&!#ep4|j|r%Qs6|>a5_th=~lpLpx2c zm68*VypD8JPS9t|Wtz3HIUDk@`OwZTyY&p2g_O3)IWHxtlSPcO^E4q3F<(v{uq89q zC;OEjA0Hg1qN!KHD}*g6 z`1_oC_Z=H>7DRhw{(|kIB{|HiKUy!z^oUfci@B`W47ssZ3@NdW!%+G6<4`FS3(a;^ z!Yy`m#~YOO7BDgdb?u|U8#FVo6ZrkD(F}XKDjNPpb~OC0@N`(#4XmdZIQ^Dix8$q{ zY2{COI<%*7z5T#{)FW!Ryqr^Y+s;xQgF;yFeC-&M31u(HNeWh`Ze6yPqYnRk->?{i z^!HbBes>BHq2tJKeEnxM?;3RA+S`yp=-gTlT$aAxrUu_H;%f} z8NZ!E8oFZ`%u(1Vn;L^lvYxi_>8F;PxHv~R6pw)ior+_A!yN*C3uBQ2oVy+SUKaw5 zx_HN6=ByWzkdqiIQ5F;pIgT6Cg8N^<@5JE8c)e55pr=#Ikw(#n&V4gOm~QLjw@Rm_ z_vSPA&uu=d`$-^{l9u&g?aob#y$GsaXv0KoNiZ~MuF7f26zd7W@@C5zJk?}TP$6G9 zmpSa7(Vs8(qDN#7K`2)bG0aRg9T+jgofQ1f- zUB(tQ4uUw3ws^-woXudJHm3l2*gcS_TA5SH<@U3ne+$on&DYA_fU-FX1>SRH{WI{@ zP<{R(L#2!0D2S5bhjl=ON;WLb?M-mM;WF|!% z{F}}BC9?i$k8$~_2*M#0iJFF&Vmy#~HAv6`2 znK*~5%!)1|9&ZyQ07z$8D7@FW^a$uPM3}$_lIXes5@YDa4Q)to&TGJMjl|}KBpnF3 z#Lg_d1vGa7(H*cEEW+k-BZZ;)2ZBvwLVwC-8b9q?U&wYF|R4NcO zz8^+&<10T?Qn|%Y8gqgBJIYbu>tNg)V^K7V=^`??7^jo~6S>7BD@O#gbb9NEE z#zy1~n0kb78q*9y5Ct&1z!Q(1*y>0ytU-Wb7fAk*bmylN{0OF>1%!=?Ag0{S)4i}% zq!*6&z}j=?N%Q#Re75=mv^OT*Am^c%9DAZxKv2>m_V1Uo*g(=a&pwi_H81S`mI#3` z?MO|B->!9HWuRapqHSHv#;FQhlX?DSo2pnH8@GZ(UV6g?>YToZ|<$n6LkC%b>1*WK1xRHAI z(2Y7sk_mA2KX!*sF%Ci-Yxcn*keuxq;G&o)f)p@CgaX5Z0Byl1Dc+Y(cxd495jd}W zUVJh&qy>86&mk6&mo{rE%rpoC757-eN)<4UfiUC|Kva2X}j@`;?^xZag7 z(9e;IpA+-su@>Za?;|3;Y$`c{KND;QS$80Ur8rIO|Z|$$A() zIV0_lL~|OGvbvY`aE~Z_IQ`4|OMz0;e+nx9DJa7g_6XpHxRc;otTJ$69yKRx1yT6r zFeKq4D!3s9FkE0d8!n(#2xkGZaP9jkxc1Mw_yw=`R52`Ch%tDBp{|vS&A(G)gu51r z!kl=y z;C}tTKkS{U=6k2R9GrdJI=;~(Nr87QE9CLTssdzs6Z5=_k^(2!spUQqzBbY6vOqHk zc`@!!9Tmz-U61w?@x(NRk-k9rcSG1fA}U=Pdp3l|&J2hghZ(vUt}WWao4ul40mFwt zUqK?-*8;)EN}X>o5ZAS7?diEX^VX$-{U|b&p0f`SU)6QZLOqptjqtx!Jj|8+eSM z75*syQZcr;7+#KeS?`zn)6i>GXBF8DcIM4sq=$)4>?4WM3i_SCfHFZpkX1{l@ z*RH0St3u8K`GzEp%a2bVHi=jPAQE7PuQGN*zW@eC;Klet3dVB~{xbqVu?f4} zxUhYkfOVK)Fma{}Mg9b;3qEP~ReWPj1!|`6a|{sof$!l!dnqO`HP-n0c2oV^_`fVUtMv#G}7vYwC|5YZO!3;~4z@SFb4a3+gINz6(Ii=eo|UR4XOmB{G@beVzx zJSdnJ3LMnDup|LA<#Y52n(gF3s$V}a7X(;L0qrcYFz4SANO%{2T6PO3jf)veGG0jP zkbz)#{q9Sd)tYfFd_-SL$4;|0H_i6 zP_Fa@wz4k4U}!4=#LmFYn6m&9%r66=hWknHvbwUPDJIXXpK>{4c( zB-)+!6#n>~OSLQAiU|QB#}8ZYP0;Ni*$Dn`xfN!%Ss44yVV2A)4u=T=#s*3pw z!WJyb(kIQiMsd)CNPYkx<&laNi3$M2tpq@uS?eu~ju?hUd<}$D*2^~|1~@dwTcyM{ zntT&5Rtg_SHQcHdaOycQq-@OI5F~B6%tiHgt;T8bFsF8iKf_)#@fh(PzzaE_&fE06 zqg7nLY;alcz)=f4TbqXxEwJG^?RMjBd|-3L{k7GwgvNSS7ObP!(K`*Bb9IRj&S0h5 zDbW`(Q3(<*tuzqHSwt_s*f$7qFO+C!&kFT>Ik!Bj1ybrcn5sqZ+9FNG&5;+_(CC^f z7U}Crdp~zglI{~5hCygblozbgt$-1t&x`P2cyt)x7zJ_W-ABc)I}n#Dr*LNap-d8c zxwJVqxI_UNgb;VC9SFn)bBNk}GcxkW?|^41{G(=xaOPmnJ%3aVy_l9CY=3er_v!U} z>+^iy1*wl$hp=3i;W<6g;caA_JGyUD{dK%&vKJ{${I(}sc4kWd1K0B&j;nH0?Sq|B zxfswS8OoaRF|AAC5Zx?X6bnH6fl8NMkrjp21y+5jl%OBq=r*(oYnlMrT%WrpjX-|h4L{=Wa+*SY6BXSw%0&vTyVeV)hCG07F;jNJKy zeYTylQI0PM{Vq&AFZLtEfx|o>ox%)ewf{g_JZKE>E|BJILjhZuB{eFr<(q5u zfbluX0tm3gg->sEj7G>>9|CQ5YymZo$Ituy+AX^ z?Iu^^;?u*`o6kli^;2)RVb2Rno@8oXZ_crn`fzO{I{AnRRs6cDSld4f`CD{s`X{}4 zQuj$1e+1SNq)w9C)Tof((Vr$iA;3)JUZ3;$lB6AE=3Pyis+!hKt_m@o-n%ew)?U0i z-StSFzhV0BaYaa2vz^sW^+h-sVFfp$%fUb_4;Sz#LOAJd!5b(kq_@KN$IUjarlHPa^PvdR5XtH z2pqh-xSQyw|tH zYEB2ygwx;sIq%ySq^7Y^OaF2?#K7dGoK;;P&0RoMP5pz;62~EKZ_^WlrO&3?xh}k> zNP{-=etqHos}Y0jSm1C&qX=;2!MP<}974-och%Fr-BIHE5kS%S=|>h=|I_>UH(dZ4 zoY#6OAt)ALj8$%qb8f6+tnG}Ec!MkJ{Jc8MvhKPoW)>g+1R>6!NE3w5?wa>>Yj{cymS>@lXy0;QKBf~S+%pp?0A<(f& zR)+Vw-EPm%v*!#J{H8`1T6?{*Pu;tPbFX_CZ+e}u>Bmzft53Vt+$Pr;OS*I;i>96y zEt&}wG>1eU=)23g5V#=lEhNmpq(34!fbd#lqkBcxo0LZ^MpnO=3}!6bcV#2q-qogN zW7iIf+*;+~k=#9yaJ-f&&X3M3+TfIf9!3V$m@@0roI>LjBsKm zoJO^Ln!3EA2TX0Hc}$-lOR2E;%M?kd9nCux=ogWwGr$~Ix`|0Rf9|00O**5_W_fm3 zebm+hr5Gp4+pKMhfA~C!_U6ry$buw0{=A`_`q_VE4}_(ke(RN2^Mm*DvJF%E(AdM5 zWeEkg{&LUS8TmC9jE_Io)L=0O)3WLwWjupN%i>brWlL@g=luS7bVWLb4XK$%wA;tF zM6T#n^MCZ&mCmW9?$?-`R@4dkdQov6)?FVNH7IzwOlQe?suKU0(g6)-jR&b>UfqWW?hMr96W&lUAn*2wPq1acRGmuCXEzg=(ty1APe&l!-^cYXmC!Y0Cw;k23`OA;q2=Agy zm@}@EDD}jdO9Dtc27Q6T>Eh3)p&fUm|1~NKS8h2~PJXw>C*S?b%9KU!K~GYF^$MxE z{qh;W0_7{(zkvV;_})Oljem!+>r362b_H6tq4!{K>xH4q8&1jOTw%7oG^`Fn^6KL- zd-T&l1n>Vg+AUkzX`uQ&&i#_VW=G$fJD$3v!F#TEBs!Fp`%JMeXNg}u?&rP1`FS^KXGjBXOVwnHeSZ0%PB>!Jy z8OKDy<7V}QtHGblG`4b2XyJ4o_)vVI#<_17Nykr~E}n~fw7H;@!{Ro^tQ}VqSyRh=fde76nS$~R|nY;9=YdbIT$A_E={TIJ*6=? z%&m7a{_y&fSeckp&&@Zi@Ac~M4lW#>qQK!VLXcJ{g<*$rY7YxfV2Cn5@dX8Uh?XrvJF z6c>BSB;Sy{6(XLj$kmUCPl31&L`iRSN9Ah%tMj#+bt`$nX|>Ioy-M^^@ou5;clh|q@Lol$BU{aFIr&6P#3PG5Ksi)?y0ahi>;@f6 zEdti^;fP>1QrR$d>-N5C|A;|=(;nYCWELvCE3S=+jTCeQ^HVx`FqF#FIunMi4 z^v1po1HDx0mtEK0x{;?8qsf+YoLsp+pL71;l6uV|qoyCqT$}K{o|DyKB9xq$-1l8b zLd3WiBw3{@CrbpP=5oahOMNQt889_Nb{Z#oyioi0oMw{;WhqRcoHB6ovw?g5(vj`t zlBP`q7wpb4Tk!6wcgtA?W?_whsC<=@>8@z+7YsYaAKo7wI2^&5{PJkbK(7z8P48N- zH+3n_EdJ)?@KH_|Bd06-UY@VKYEpYHC9y6CxBv1bSHrgL(V|V-^%^59juMYMhrXE) zvMwANV!c}&m&m?8naDEX5_3)W$%~?^rgcLgNw73YX=Rlgc;Wdq3} z=+7H@i_<2Che6!yhRkWr8e`>{1t#|!$E?H$w%8vm|L2h_HQsGIJkxpqFl+ly_e=wc z!-8$=4EkdatL~m~#M}6ZCx!g4nYFE6$0ozE2BDXnu^&73hovhg|F3b)qT{{)lKfu_ zUih-+Uuyr~Lgns>mH#$IA9)W-Scxa$HF6gXo1ZkkO#A$CdCXlAt~%Cpa8K@2M}$*S zl_mo0D|Y2lGb(^xQ3Q|^0oazn3a$Rwr(=PhTy?rL?2U5XNmu-vo~}2Ci`Cs_W(fHH ztLar8RqomH%4GHX;C(;n)k<|4kF5$rZdD|1IKymcjPq)@^h&wuq5WDa z@f)#w7#4x27?t4AVG?Qfo@GWB`Yq{Iu3oq7NhR*=+TtmBczu-K*$R$FtX&HE(-OLF&b7KGm@@<(O@POANxg0V3B( zF-yJ9ZWT9Qc&#=!_!Qvh$J5_17FA-2Y{;*(La%*B-;Vo}`k!k~x@I(a?dbR{#hEmG ze{|$|9Y{*G?C;LI6}o+r+()}UQX%~HebWxfa5+mw^NxSht&wKOZI;!_uY%eGFRzdz ze6J)&NqgMneDfpzP#8|E`P@9m;;%_;8)UryIdW(A63tbbDG*EIf3ih7t4KO?xMyXz z87bJ9o(=1I5yGY4v`WEJRWGD}h^X)8fEIxfSu_cGKAZ?u6wMo~ZMtXecn~3U9WgZ=g z*aPuSSNHj^o7=c33O<(K^n*j=3CN#M^vWG|-0JRxVK>QkMwRU;%O%>v6dM9oh zOSlM@&<^Yo{nj?(9(uU2Ac(NpV z5Pyfu0Dm~Oz~i4U=)7RI`p?vz#l=G)&{HGmMW@u&^WkC2tl6uy^DVD>n&Kl^53_4756p>r(%i zCjAi1*8q!8P_dy)-L4}A?e7w4=Dm)pZrGHy8w{ai=~v)M^CIU4nLPtR@1ebyrBth9 z*8>>Van*m`q`{6|CJ4O;TyC&7U8%IFua)3Rm@0-#I^~)^d2byCFB?ba+Nx1 z#KSQ$kHQkP3XzP)PKbW!G%I<&*tKVWy64HNdbDPq?z2pIPlSu zN+?fi{0>Prw%emnCvOIHkqEx3JoTqf!1}*P)jPQ-b~A31wYcoeMU{m>&dkIM1YdDt zoZM+^UY^*+LZe1PnP)DkPc1}FV^d2lSJ51c_AA@CBcFrjFKOzx;N%(m)i(nQ#`YwG zr*tUmwK2^36VN{pdR3_eWpFf$UF!im#Nr|qsqa`hotJMc+}zPe@= z_1sJ}>#Bto159t3lywOCaGVsYUcN=amLf)yCMS09XOM}9dF?uYCh# ze`(bIEyBem0k+^o;PY=$I$E>_q<%tWF|#CguD_itXd+QCX)hgznWa_kX)pGK9b*%K z1i%_1tS=6MlXQaDEXmmqW~D>iYovp%6!A3Rpox4!GchQ10kp$%w?yy^u6&Koz*zbExeN43Q8Edw7qE#d0^E}5Ju7Vl^(mqyeiR()#z_Wp z4anq*1mgIWhOL9eV?kUp*zrz8NAtI&aa%kI@Ja%$8@r?*CwKAaxPC7}CwGZ^=^!C} z7NuG42Mf9Y&LY4s32<}UP*W6=+x-C=M1>b3jxKp~)-3M#zqNkAoJl}X5)l5aMHf#` z0MD}l^WSxa5+Wn55=s%)9#G;3XFPcLpT+P!CWzj)x`1|Sg?`k(ivg)G&fw%RWyY|0 zvGnVj7%M#&6(nN<6r>bCfir?Vv?a%Yi29ebt20!!r=rlj4KsP`F?w^lwo$>@y(E@~ zRcg;&4XnmMb%2*!5S-iLoLbf2Aq3Er7!cR?+|l{FVsO?rXE+XIXw@~~bP@I}6dzsb z%tSVpzTCylhqBLwSAgvg#N`5a@>4|(56>)AC3g+9+L`AF^j!;!x)v$WHzE?U*)yU8 zS<18u*Z2K|E7$Kl5NJQ!9vUq|E;bJ4T7?~z((#YLUfYWxgNHnMo_1eGq~`4mz^@T1 z3;TT%FWnZ!$jzbAH@JYe^Zjhq z2xNYmJwDDMI-0uVo)JZ|syLI82$aTXcuB;qxG#hq6-o?{Ty4s=S6{)6TEhc>Anc5| zyH#i4DJdHvOY9MIpqwtI5L<8(R86B2_)x5WnjvyBbqn* z5(Cp)b6yI6C&}AMC8b<6&EIS}C+w{hJXsC0^0Jt8)04cbZB}WC$P|};j zse!z`;OqyW#kfPn!7t2H`(k-@B*__y>0%UI!MG@h&EL-giMHfo;rl*?GP+<-%9Aq} zQby^ONu`#bpoVc5&?z*_nq1sy6^-vkUpaJfNeMX^;39` z2?nxAXjWLiFF2;mPzB!J2SRot#j{Il-3g#{L=46z1L6NiQ|=>Idc$^c0Y^#Gr1>~d zoJl82ASk12d-6*oZ&=4AJb-~#c|;$yDS-469y)G*mXe0S&~z#D;~*mIZ6=tT#hRef zn12a0mD!HYG9c6lk^;m0j#_sI0X5?r*$?qhz>z~^&|7EjW38i*Wx>$U1_}=ifdy|q z&t)Lbvq4_>2I!E(0s`^-0{&6xa$!4BlH)Q4Jm|#9n)>rS0)DRxg&_VmNJ9>!Hmq?z zQArBbcXsH^RUy0P5m0!n&7)RAM+YQj#bR5@!LB8n&#M!T6L<8K2e~%Nhy^F2Ci~vx zub4HLX~~s?L)MfT(LI&^BmtlzrZabbVzuxU`1Bh^6Q2N;Z^)|(7BD{s!cO<^R2y(2 zdw+w?1BhNf5@Z0UThsy0ss0^)*+^RM{^Xz@C`D;}2H4~$TFqZJJBN;Gq#$2=(r~6y z(FSyW1AgGXx=d<|FxhT~mAV=0a0o=iIfbQ!_1}XQXhbUTqTCKF*po2uEB~L?>R9z~ z$Yj>PB`_2^6@*@Du$bcp$$}Kzne6U)wKIXR8b`gR%UPJ89Yr`A7944ZlD=H%ZXd1{ z#z0~fC-1PFmYFe}A$E<}q8@VF=|^CeDp{(B1W)jvXrM#2s5HE}K`4XehHsc5VigZ& zad+4aJ}Nk~9a+{Y ze>a45txmZ$70S2Gx&$2K=#}F}AMSI1I1~vv5yH6= zMIzK+RmA1C3{t3*^)x7shCi!#)vAMTj7_p+=G3dt1C8W^N~Z2zO~jh#O$v4iww7X8^SIYq)2U zw+WI*uK=YSATS568&=BR38_~kf(z5jxS-3Zk`%;^9CI3Ke{rW|hStf|$mfh-`^Mm; zjQj;37B?0lsq=DWi#xz7M*T|SGn60li)}z_Gu!+!ZwtYj#4SHUb zVw=Ieb@mAIDc#LjBNQj03g_&D!znd)ULK3}hjU*c`0gHvVFQ|IgZ3mx$rZ4aGEb50 zzGQqLl>hn#)X)U!cos4?8mhw3N=FK|aK|YbA1$ZF_mj66k$1*Xq}P1NI}9?#C|KX( RkUR>QwoLZi!*H=-{ttLP47>mU diff --git a/src/libs/python/bongo/external/email/__init__.py b/src/libs/python/bongo/external/email/__init__.py deleted file mode 100644 index 2b94fd5..0000000 --- a/src/libs/python/bongo/external/email/__init__.py +++ /dev/null @@ -1,123 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Barry Warsaw -# Contact: email-sig@python.org - -"""A package for parsing, handling, and generating email messages.""" - -__version__ = '4.0a2' - -__all__ = [ - # Old names - 'base64MIME', - 'Charset', - 'Encoders', - 'Errors', - 'Generator', - 'Header', - 'Iterators', - 'Message', - 'MIMEAudio', - 'MIMEBase', - 'MIMEImage', - 'MIMEMessage', - 'MIMEMultipart', - 'MIMENonMultipart', - 'MIMEText', - 'Parser', - 'quopriMIME', - 'Utils', - 'message_from_string', - 'message_from_file', - # new names - 'base64mime', - 'charset', - 'encoders', - 'errors', - 'generator', - 'header', - 'iterators', - 'message', - 'mime', - 'parser', - 'quoprimime', - 'utils', - ] - - - -# Some convenience routines. Don't import Parser and Message as side-effects -# of importing email since those cascadingly import most of the rest of the -# email package. -def message_from_string(s, *args, **kws): - """Parse a string into a Message object model. - - Optional _class and strict are passed to the Parser constructor. - """ - from bongo.external.email.parser import Parser - return Parser(*args, **kws).parsestr(s) - - -def message_from_file(fp, *args, **kws): - """Read a file and parse its contents into a Message object model. - - Optional _class and strict are passed to the Parser constructor. - """ - from bongo.external.email.parser import Parser - return Parser(*args, **kws).parse(fp) - - - -# Lazy loading to provide name mapping from new-style names (PEP 8 compatible -# email 4.0 module names), to old-style names (email 3.0 module names). -import sys - -class LazyImporter(object): - def __init__(self, module_name): - self.__name__ = 'bongo.external.email.' + module_name - - def __getattr__(self, name): - __import__(self.__name__) - mod = sys.modules[self.__name__] - self.__dict__.update(mod.__dict__) - return getattr(mod, name) - - -_LOWERNAMES = [ - # email. -> email. - 'Charset', - 'Encoders', - 'Errors', - 'FeedParser', - 'Generator', - 'Header', - 'Iterators', - 'Message', - 'Parser', - 'Utils', - 'base64MIME', - 'quopriMIME', - ] - -_MIMENAMES = [ - # email.MIME -> email.mime. - 'Audio', - 'Base', - 'Image', - 'Message', - 'Multipart', - 'NonMultipart', - 'Text', - ] - -for _name in _LOWERNAMES: - importer = LazyImporter(_name.lower()) - sys.modules['bongo.external.email.' + _name] = importer - setattr(sys.modules['bongo.external.email'], _name, importer) - - -import bongo.external.email.mime -for _name in _MIMENAMES: - importer = LazyImporter('mime.' + _name.lower()) - sys.modules['bongo.external.email.MIME' + _name] = importer - setattr(sys.modules['bongo.external.email'], 'MIME' + _name, importer) - setattr(sys.modules['bongo.external.email.mime'], _name, importer) diff --git a/src/libs/python/bongo/external/email/_parseaddr.py b/src/libs/python/bongo/external/email/_parseaddr.py deleted file mode 100644 index 8047df2..0000000 --- a/src/libs/python/bongo/external/email/_parseaddr.py +++ /dev/null @@ -1,479 +0,0 @@ -# Copyright (C) 2002-2006 Python Software Foundation -# Contact: email-sig@python.org - -"""Email address parsing code. - -Lifted directly from rfc822.py. This should eventually be rewritten. -""" - -__all__ = [ - 'mktime_tz', - 'parsedate', - 'parsedate_tz', - 'quote', - ] - -import time - -SPACE = ' ' -EMPTYSTRING = '' -COMMASPACE = ', ' - -# Parse a date field -_monthnames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul', - 'aug', 'sep', 'oct', 'nov', 'dec', - 'january', 'february', 'march', 'april', 'may', 'june', 'july', - 'august', 'september', 'october', 'november', 'december'] - -_daynames = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'] - -# The timezone table does not include the military time zones defined -# in RFC822, other than Z. According to RFC1123, the description in -# RFC822 gets the signs wrong, so we can't rely on any such time -# zones. RFC1123 recommends that numeric timezone indicators be used -# instead of timezone names. - -_timezones = {'UT':0, 'UTC':0, 'GMT':0, 'Z':0, - 'AST': -400, 'ADT': -300, # Atlantic (used in Canada) - 'EST': -500, 'EDT': -400, # Eastern - 'CST': -600, 'CDT': -500, # Central - 'MST': -700, 'MDT': -600, # Mountain - 'PST': -800, 'PDT': -700 # Pacific - } - - -def parsedate_tz(data): - """Convert a date string to a time tuple. - - Accounts for military timezones. - """ - data = data.split() - # The FWS after the comma after the day-of-week is optional, so search and - # adjust for this. - if data[0].endswith(',') or data[0].lower() in _daynames: - # There's a dayname here. Skip it - del data[0] - else: - i = data[0].rfind(',') - if i >= 0: - data[0] = data[0][i+1:] - if len(data) == 3: # RFC 850 date, deprecated - stuff = data[0].split('-') - if len(stuff) == 3: - data = stuff + data[1:] - if len(data) == 4: - s = data[3] - i = s.find('+') - if i > 0: - data[3:] = [s[:i], s[i+1:]] - else: - data.append('') # Dummy tz - if len(data) < 5: - return None - data = data[:5] - [dd, mm, yy, tm, tz] = data - mm = mm.lower() - if mm not in _monthnames: - dd, mm = mm, dd.lower() - if mm not in _monthnames: - return None - mm = _monthnames.index(mm) + 1 - if mm > 12: - mm -= 12 - if dd[-1] == ',': - dd = dd[:-1] - i = yy.find(':') - if i > 0: - yy, tm = tm, yy - if yy[-1] == ',': - yy = yy[:-1] - if not yy[0].isdigit(): - yy, tz = tz, yy - if tm[-1] == ',': - tm = tm[:-1] - tm = tm.split(':') - if len(tm) == 2: - [thh, tmm] = tm - tss = '0' - elif len(tm) == 3: - [thh, tmm, tss] = tm - else: - return None - try: - yy = int(yy) - dd = int(dd) - thh = int(thh) - tmm = int(tmm) - tss = int(tss) - except ValueError: - return None - tzoffset = None - tz = tz.upper() - if tz in _timezones: - tzoffset = _timezones[tz] - else: - try: - tzoffset = int(tz) - except ValueError: - pass - # Convert a timezone offset into seconds ; -0500 -> -18000 - if tzoffset: - if tzoffset < 0: - tzsign = -1 - tzoffset = -tzoffset - else: - tzsign = 1 - tzoffset = tzsign * ( (tzoffset//100)*3600 + (tzoffset % 100)*60) - # Daylight Saving Time flag is set to -1, since DST is unknown. - return yy, mm, dd, thh, tmm, tss, 0, 1, -1, tzoffset - - -def parsedate(data): - """Convert a time string to a time tuple.""" - t = parsedate_tz(data) - if isinstance(t, tuple): - return t[:9] - else: - return t - - -def mktime_tz(data): - """Turn a 10-tuple as returned by parsedate_tz() into a UTC timestamp.""" - if data[9] is None: - # No zone info, so localtime is better assumption than GMT - return time.mktime(data[:8] + (-1,)) - else: - t = time.mktime(data[:8] + (0,)) - return t - data[9] - time.timezone - - -def quote(str): - """Add quotes around a string.""" - return str.replace('\\', '\\\\').replace('"', '\\"') - - -class AddrlistClass: - """Address parser class by Ben Escoto. - - To understand what this class does, it helps to have a copy of RFC 2822 in - front of you. - - Note: this class interface is deprecated and may be removed in the future. - Use rfc822.AddressList instead. - """ - - def __init__(self, field): - """Initialize a new instance. - - `field' is an unparsed address header field, containing - one or more addresses. - """ - self.specials = '()<>@,:;.\"[]' - self.pos = 0 - self.LWS = ' \t' - self.CR = '\r\n' - self.atomends = self.specials + self.LWS + self.CR - # Note that RFC 2822 now specifies `.' as obs-phrase, meaning that it - # is obsolete syntax. RFC 2822 requires that we recognize obsolete - # syntax, so allow dots in phrases. - self.phraseends = self.atomends.replace('.', '') - self.field = field - self.commentlist = [] - - def gotonext(self): - """Parse up to the start of the next address.""" - while self.pos < len(self.field): - if self.field[self.pos] in self.LWS + '\n\r': - self.pos += 1 - elif self.field[self.pos] == '(': - self.commentlist.append(self.getcomment()) - else: - break - - def getaddrlist(self): - """Parse all addresses. - - Returns a list containing all of the addresses. - """ - result = [] - while self.pos < len(self.field): - ad = self.getaddress() - if ad: - result += ad - else: - result.append(('', '')) - return result - - def getaddress(self): - """Parse the next address.""" - self.commentlist = [] - self.gotonext() - - oldpos = self.pos - oldcl = self.commentlist - plist = self.getphraselist() - - self.gotonext() - returnlist = [] - - if self.pos >= len(self.field): - # Bad email address technically, no domain. - if plist: - returnlist = [(SPACE.join(self.commentlist), plist[0])] - - elif self.field[self.pos] in '.@': - # email address is just an addrspec - # this isn't very efficient since we start over - self.pos = oldpos - self.commentlist = oldcl - addrspec = self.getaddrspec() - returnlist = [(SPACE.join(self.commentlist), addrspec)] - - elif self.field[self.pos] == ':': - # address is a group - returnlist = [] - - fieldlen = len(self.field) - self.pos += 1 - while self.pos < len(self.field): - self.gotonext() - if self.pos < fieldlen and self.field[self.pos] == ';': - self.pos += 1 - break - returnlist = returnlist + self.getaddress() - - elif self.field[self.pos] == '<': - # Address is a phrase then a route addr - routeaddr = self.getrouteaddr() - - if self.commentlist: - returnlist = [(SPACE.join(plist) + ' (' + - ' '.join(self.commentlist) + ')', routeaddr)] - else: - returnlist = [(SPACE.join(plist), routeaddr)] - - else: - if plist: - returnlist = [(SPACE.join(self.commentlist), plist[0])] - elif self.field[self.pos] in self.specials: - self.pos += 1 - - self.gotonext() - if self.pos < len(self.field) and self.field[self.pos] == ',': - self.pos += 1 - return returnlist - - def getrouteaddr(self): - """Parse a route address (Return-path value). - - This method just skips all the route stuff and returns the addrspec. - """ - if self.field[self.pos] != '<': - return - - expectroute = False - self.pos += 1 - self.gotonext() - adlist = '' - while self.pos < len(self.field): - if expectroute: - self.getdomain() - expectroute = False - elif self.field[self.pos] == '>': - self.pos += 1 - break - elif self.field[self.pos] == '@': - self.pos += 1 - expectroute = True - elif self.field[self.pos] == ':': - self.pos += 1 - else: - adlist = self.getaddrspec() - self.pos += 1 - break - self.gotonext() - - return adlist - - def getaddrspec(self): - """Parse an RFC 2822 addr-spec.""" - aslist = [] - - self.gotonext() - while self.pos < len(self.field): - if self.field[self.pos] == '.': - aslist.append('.') - self.pos += 1 - elif self.field[self.pos] == '"': - aslist.append('"%s"' % self.getquote()) - elif self.field[self.pos] in self.atomends: - break - else: - aslist.append(self.getatom()) - self.gotonext() - - if self.pos >= len(self.field) or self.field[self.pos] != '@': - return EMPTYSTRING.join(aslist) - - aslist.append('@') - self.pos += 1 - self.gotonext() - return EMPTYSTRING.join(aslist) + self.getdomain() - - def getdomain(self): - """Get the complete domain name from an address.""" - sdlist = [] - while self.pos < len(self.field): - if self.field[self.pos] in self.LWS: - self.pos += 1 - elif self.field[self.pos] == '(': - self.commentlist.append(self.getcomment()) - elif self.field[self.pos] == '[': - sdlist.append(self.getdomainliteral()) - elif self.field[self.pos] == '.': - self.pos += 1 - sdlist.append('.') - elif self.field[self.pos] in self.atomends: - break - else: - sdlist.append(self.getatom()) - return EMPTYSTRING.join(sdlist) - - def getdelimited(self, beginchar, endchars, allowcomments=True): - """Parse a header fragment delimited by special characters. - - `beginchar' is the start character for the fragment. - If self is not looking at an instance of `beginchar' then - getdelimited returns the empty string. - - `endchars' is a sequence of allowable end-delimiting characters. - Parsing stops when one of these is encountered. - - If `allowcomments' is non-zero, embedded RFC 2822 comments are allowed - within the parsed fragment. - """ - if self.field[self.pos] != beginchar: - return '' - - slist = [''] - quote = False - self.pos += 1 - while self.pos < len(self.field): - if quote: - slist.append(self.field[self.pos]) - quote = False - elif self.field[self.pos] in endchars: - self.pos += 1 - break - elif allowcomments and self.field[self.pos] == '(': - slist.append(self.getcomment()) - continue # have already advanced pos from getcomment - elif self.field[self.pos] == '\\': - quote = True - else: - slist.append(self.field[self.pos]) - self.pos += 1 - - return EMPTYSTRING.join(slist) - - def getquote(self): - """Get a quote-delimited fragment from self's field.""" - return self.getdelimited('"', '"\r', False) - - def getcomment(self): - """Get a parenthesis-delimited fragment from self's field.""" - return self.getdelimited('(', ')\r', True) - - def getdomainliteral(self): - """Parse an RFC 2822 domain-literal.""" - return '[%s]' % self.getdelimited('[', ']\r', False) - - def getatom(self, atomends=None): - """Parse an RFC 2822 atom. - - Optional atomends specifies a different set of end token delimiters - (the default is to use self.atomends). This is used e.g. in - getphraselist() since phrase endings must not include the `.' (which - is legal in phrases).""" - atomlist = [''] - if atomends is None: - atomends = self.atomends - - while self.pos < len(self.field): - if self.field[self.pos] in atomends: - break - else: - atomlist.append(self.field[self.pos]) - self.pos += 1 - - return EMPTYSTRING.join(atomlist) - - def getphraselist(self): - """Parse a sequence of RFC 2822 phrases. - - A phrase is a sequence of words, which are in turn either RFC 2822 - atoms or quoted-strings. Phrases are canonicalized by squeezing all - runs of continuous whitespace into one space. - """ - plist = [] - - while self.pos < len(self.field): - if self.field[self.pos] in self.LWS: - self.pos += 1 - elif self.field[self.pos] == '"': - plist.append(self.getquote()) - elif self.field[self.pos] == '(': - self.commentlist.append(self.getcomment()) - elif self.field[self.pos] in self.phraseends: - break - else: - plist.append(self.getatom(self.phraseends)) - - return plist - -class AddressList(AddrlistClass): - """An AddressList encapsulates a list of parsed RFC 2822 addresses.""" - def __init__(self, field): - AddrlistClass.__init__(self, field) - if field: - self.addresslist = self.getaddrlist() - else: - self.addresslist = [] - - def __len__(self): - return len(self.addresslist) - - def __add__(self, other): - # Set union - newaddr = AddressList(None) - newaddr.addresslist = self.addresslist[:] - for x in other.addresslist: - if not x in self.addresslist: - newaddr.addresslist.append(x) - return newaddr - - def __iadd__(self, other): - # Set union, in-place - for x in other.addresslist: - if not x in self.addresslist: - self.addresslist.append(x) - return self - - def __sub__(self, other): - # Set difference - newaddr = AddressList(None) - for x in self.addresslist: - if not x in other.addresslist: - newaddr.addresslist.append(x) - return newaddr - - def __isub__(self, other): - # Set difference, in-place - for x in other.addresslist: - if x in self.addresslist: - self.addresslist.remove(x) - return self - - def __getitem__(self, index): - # Make indexing, slices, and 'in' work - return self.addresslist[index] diff --git a/src/libs/python/bongo/external/email/base64mime.py b/src/libs/python/bongo/external/email/base64mime.py deleted file mode 100644 index 4fa8563..0000000 --- a/src/libs/python/bongo/external/email/base64mime.py +++ /dev/null @@ -1,184 +0,0 @@ -# Copyright (C) 2002-2006 Python Software Foundation -# Author: Ben Gertzfield -# Contact: email-sig@python.org - -"""Base64 content transfer encoding per RFCs 2045-2047. - -This module handles the content transfer encoding method defined in RFC 2045 -to encode arbitrary 8-bit data using the three 8-bit bytes in four 7-bit -characters encoding known as Base64. - -It is used in the MIME standards for email to attach images, audio, and text -using some 8-bit character sets to messages. - -This module provides an interface to encode and decode both headers and bodies -with Base64 encoding. - -RFC 2045 defines a method for including character set information in an -`encoded-word' in a header. This method is commonly used for 8-bit real names -in To:, From:, Cc:, etc. fields, as well as Subject: lines. - -This module does not do the line wrapping or end-of-line character conversion -necessary for proper internationalized headers; it only does dumb encoding and -decoding. To deal with the various line wrapping issues, use the email.Header -module. -""" - -__all__ = [ - 'base64_len', - 'body_decode', - 'body_encode', - 'decode', - 'decodestring', - 'encode', - 'encodestring', - 'header_encode', - ] - -import re - -from binascii import b2a_base64, a2b_base64 -from bongo.external.email.utils import fix_eols - -CRLF = '\r\n' -NL = '\n' -EMPTYSTRING = '' - -# See also Charset.py -MISC_LEN = 7 - - - -# Helpers -def base64_len(s): - """Return the length of s when it is encoded with base64.""" - groups_of_3, leftover = divmod(len(s), 3) - # 4 bytes out for each 3 bytes (or nonzero fraction thereof) in. - # Thanks, Tim! - n = groups_of_3 * 4 - if leftover: - n += 4 - return n - - - -def header_encode(header, charset='iso-8859-1', keep_eols=False, - maxlinelen=76, eol=NL): - """Encode a single header line with Base64 encoding in a given charset. - - Defined in RFC 2045, this Base64 encoding is identical to normal Base64 - encoding, except that each line must be intelligently wrapped (respecting - the Base64 encoding), and subsequent lines must start with a space. - - charset names the character set to use to encode the header. It defaults - to iso-8859-1. - - End-of-line characters (\\r, \\n, \\r\\n) will be automatically converted - to the canonical email line separator \\r\\n unless the keep_eols - parameter is True (the default is False). - - Each line of the header will be terminated in the value of eol, which - defaults to "\\n". Set this to "\\r\\n" if you are using the result of - this function directly in email. - - The resulting string will be in the form: - - "=?charset?b?WW/5ciBtYXp66XLrIHf8eiBhIGhhbXBzdGHuciBBIFlv+XIgbWF6euly?=\\n - =?charset?b?6yB3/HogYSBoYW1wc3Rh7nIgQkMgWW/5ciBtYXp66XLrIHf8eiBhIGhh?=" - - with each line wrapped at, at most, maxlinelen characters (defaults to 76 - characters). - """ - # Return empty headers unchanged - if not header: - return header - - if not keep_eols: - header = fix_eols(header) - - # Base64 encode each line, in encoded chunks no greater than maxlinelen in - # length, after the RFC chrome is added in. - base64ed = [] - max_encoded = maxlinelen - len(charset) - MISC_LEN - max_unencoded = max_encoded * 3 // 4 - - for i in range(0, len(header), max_unencoded): - base64ed.append(b2a_base64(header[i:i+max_unencoded])) - - # Now add the RFC chrome to each encoded chunk - lines = [] - for line in base64ed: - # Ignore the last character of each line if it is a newline - if line.endswith(NL): - line = line[:-1] - # Add the chrome - lines.append('=?%s?b?%s?=' % (charset, line)) - # Glue the lines together and return it. BAW: should we be able to - # specify the leading whitespace in the joiner? - joiner = eol + ' ' - return joiner.join(lines) - - - -def encode(s, binary=True, maxlinelen=76, eol=NL): - """Encode a string with base64. - - Each line will be wrapped at, at most, maxlinelen characters (defaults to - 76 characters). - - If binary is False, end-of-line characters will be converted to the - canonical email end-of-line sequence \\r\\n. Otherwise they will be left - verbatim (this is the default). - - Each line of encoded text will end with eol, which defaults to "\\n". Set - this to "\r\n" if you will be using the result of this function directly - in an email. - """ - if not s: - return s - - if not binary: - s = fix_eols(s) - - encvec = [] - max_unencoded = maxlinelen * 3 // 4 - for i in range(0, len(s), max_unencoded): - # BAW: should encode() inherit b2a_base64()'s dubious behavior in - # adding a newline to the encoded string? - enc = b2a_base64(s[i:i + max_unencoded]) - if enc.endswith(NL) and eol != NL: - enc = enc[:-1] + eol - encvec.append(enc) - return EMPTYSTRING.join(encvec) - - -# For convenience and backwards compatibility w/ standard base64 module -body_encode = encode -encodestring = encode - - - -def decode(s, convert_eols=None): - """Decode a raw base64 string. - - If convert_eols is set to a string value, all canonical email linefeeds, - e.g. "\\r\\n", in the decoded text will be converted to the value of - convert_eols. os.linesep is a good choice for convert_eols if you are - decoding a text attachment. - - This function does not parse a full MIME header value encoded with - base64 (like =?iso-8895-1?b?bmloISBuaWgh?=) -- please use the high - level email.Header class for that functionality. - """ - if not s: - return s - - dec = a2b_base64(s) - if convert_eols: - return dec.replace(CRLF, convert_eols) - return dec - - -# For convenience and backwards compatibility w/ standard base64 module -body_decode = decode -decodestring = decode diff --git a/src/libs/python/bongo/external/email/charset.py b/src/libs/python/bongo/external/email/charset.py deleted file mode 100644 index 3205b50..0000000 --- a/src/libs/python/bongo/external/email/charset.py +++ /dev/null @@ -1,398 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Ben Gertzfield, Barry Warsaw -# Contact: email-sig@python.org - -__all__ = [ - 'Charset', - 'add_alias', - 'add_charset', - 'add_codec', - ] - -import bongo.external.email.base64mime -import bongo.external.email.quoprimime - -from bongo.external.email import errors -from bongo.external.email.encoders import encode_7or8bit - - - -# Flags for types of header encodings -QP = 1 # Quoted-Printable -BASE64 = 2 # Base64 -SHORTEST = 3 # the shorter of QP and base64, but only for headers - -# In "=?charset?q?hello_world?=", the =?, ?q?, and ?= add up to 7 -MISC_LEN = 7 - -DEFAULT_CHARSET = 'us-ascii' - - - -# Defaults -CHARSETS = { - # input header enc body enc output conv - 'iso-8859-1': (QP, QP, None), - 'iso-8859-2': (QP, QP, None), - 'iso-8859-3': (QP, QP, None), - 'iso-8859-4': (QP, QP, None), - # iso-8859-5 is Cyrillic, and not especially used - # iso-8859-6 is Arabic, also not particularly used - # iso-8859-7 is Greek, QP will not make it readable - # iso-8859-8 is Hebrew, QP will not make it readable - 'iso-8859-9': (QP, QP, None), - 'iso-8859-10': (QP, QP, None), - # iso-8859-11 is Thai, QP will not make it readable - 'iso-8859-13': (QP, QP, None), - 'iso-8859-14': (QP, QP, None), - 'iso-8859-15': (QP, QP, None), - 'windows-1252':(QP, QP, None), - 'viscii': (QP, QP, None), - 'us-ascii': (None, None, None), - 'big5': (BASE64, BASE64, None), - 'gb2312': (BASE64, BASE64, None), - 'euc-jp': (BASE64, None, 'iso-2022-jp'), - 'shift_jis': (BASE64, None, 'iso-2022-jp'), - 'iso-2022-jp': (BASE64, None, None), - 'koi8-r': (BASE64, BASE64, None), - 'utf-8': (SHORTEST, BASE64, 'utf-8'), - # We're making this one up to represent raw unencoded 8-bit - '8bit': (None, BASE64, None), - } - -# Aliases for other commonly-used names for character sets. Map -# them to the real ones used in email. -ALIASES = { - 'latin_1': 'iso-8859-1', - 'latin-1': 'iso-8859-1', - 'latin_2': 'iso-8859-2', - 'latin-2': 'iso-8859-2', - 'latin_3': 'iso-8859-3', - 'latin-3': 'iso-8859-3', - 'latin_4': 'iso-8859-4', - 'latin-4': 'iso-8859-4', - 'latin_5': 'iso-8859-9', - 'latin-5': 'iso-8859-9', - 'latin_6': 'iso-8859-10', - 'latin-6': 'iso-8859-10', - 'latin_7': 'iso-8859-13', - 'latin-7': 'iso-8859-13', - 'latin_8': 'iso-8859-14', - 'latin-8': 'iso-8859-14', - 'latin_9': 'iso-8859-15', - 'latin-9': 'iso-8859-15', - 'cp949': 'ks_c_5601-1987', - 'euc_jp': 'euc-jp', - 'euc_kr': 'euc-kr', - 'ascii': 'us-ascii', - } - - -# Map charsets to their Unicode codec strings. -CODEC_MAP = { - 'gb2312': 'eucgb2312_cn', - 'big5': 'big5_tw', - # Hack: We don't want *any* conversion for stuff marked us-ascii, as all - # sorts of garbage might be sent to us in the guise of 7-bit us-ascii. - # Let that stuff pass through without conversion to/from Unicode. - 'us-ascii': None, - } - - - -# Convenience functions for extending the above mappings -def add_charset(charset, header_enc=None, body_enc=None, output_charset=None): - """Add character set properties to the global registry. - - charset is the input character set, and must be the canonical name of a - character set. - - Optional header_enc and body_enc is either Charset.QP for - quoted-printable, Charset.BASE64 for base64 encoding, Charset.SHORTEST for - the shortest of qp or base64 encoding, or None for no encoding. SHORTEST - is only valid for header_enc. It describes how message headers and - message bodies in the input charset are to be encoded. Default is no - encoding. - - Optional output_charset is the character set that the output should be - in. Conversions will proceed from input charset, to Unicode, to the - output charset when the method Charset.convert() is called. The default - is to output in the same character set as the input. - - Both input_charset and output_charset must have Unicode codec entries in - the module's charset-to-codec mapping; use add_codec(charset, codecname) - to add codecs the module does not know about. See the codecs module's - documentation for more information. - """ - if body_enc == SHORTEST: - raise ValueError('SHORTEST not allowed for body_enc') - CHARSETS[charset] = (header_enc, body_enc, output_charset) - - -def add_alias(alias, canonical): - """Add a character set alias. - - alias is the alias name, e.g. latin-1 - canonical is the character set's canonical name, e.g. iso-8859-1 - """ - ALIASES[alias] = canonical - - -def add_codec(charset, codecname): - """Add a codec that map characters in the given charset to/from Unicode. - - charset is the canonical name of a character set. codecname is the name - of a Python codec, as appropriate for the second argument to the unicode() - built-in, or to the encode() method of a Unicode string. - """ - CODEC_MAP[charset] = codecname - - - -class Charset: - """Map character sets to their email properties. - - This class provides information about the requirements imposed on email - for a specific character set. It also provides convenience routines for - converting between character sets, given the availability of the - applicable codecs. Given a character set, it will do its best to provide - information on how to use that character set in an email in an - RFC-compliant way. - - Certain character sets must be encoded with quoted-printable or base64 - when used in email headers or bodies. Certain character sets must be - converted outright, and are not allowed in email. Instances of this - module expose the following information about a character set: - - input_charset: The initial character set specified. Common aliases - are converted to their `official' email names (e.g. latin_1 - is converted to iso-8859-1). Defaults to 7-bit us-ascii. - - header_encoding: If the character set must be encoded before it can be - used in an email header, this attribute will be set to - Charset.QP (for quoted-printable), Charset.BASE64 (for - base64 encoding), or Charset.SHORTEST for the shortest of - QP or BASE64 encoding. Otherwise, it will be None. - - body_encoding: Same as header_encoding, but describes the encoding for the - mail message's body, which indeed may be different than the - header encoding. Charset.SHORTEST is not allowed for - body_encoding. - - output_charset: Some character sets must be converted before the can be - used in email headers or bodies. If the input_charset is - one of them, this attribute will contain the name of the - charset output will be converted to. Otherwise, it will - be None. - - input_codec: The name of the Python codec used to convert the - input_charset to Unicode. If no conversion codec is - necessary, this attribute will be None. - - output_codec: The name of the Python codec used to convert Unicode - to the output_charset. If no conversion codec is necessary, - this attribute will have the same value as the input_codec. - """ - def __init__(self, input_charset=DEFAULT_CHARSET, body_encoding=None): - # RFC 2046, $4.1.2 says charsets are not case sensitive. We coerce to - # unicode because its .lower() is locale insensitive. If the argument - # is already a unicode, we leave it at that, but ensure that the - # charset is ASCII, as the standard (RFC XXX) requires. - try: - if isinstance(input_charset, str): - input_charset.encode('ascii') - else: - input_charset = str(input_charset, 'ascii') - except UnicodeError: - raise errors.CharsetError(input_charset) - input_charset = input_charset.lower() - # Set the input charset after filtering through the aliases - self.input_charset = ALIASES.get(input_charset, input_charset) - # We can try to guess which encoding and conversion to use by the - # charset_map dictionary. Try that first, but let the user override - # it. - henc, benc, conv = CHARSETS.get(self.input_charset, - (SHORTEST, BASE64, None)) - if not conv: - conv = self.input_charset - # Set the attributes, allowing the arguments to override the default. - self.header_encoding = henc - - if body_encoding : - body_encoding = body_encoding.lower() - if body_encoding == 'base64' : - self.body_encoding = BASE64 - elif body_encoding == 'quoted-printable' : - self.body_encoding = QP - else : - self.body_encoding = benc - else : - self.body_encoding = benc - self.output_charset = ALIASES.get(conv, conv) - # Now set the codecs. If one isn't defined for input_charset, - # guess and try a Unicode codec with the same name as input_codec. - self.input_codec = CODEC_MAP.get(self.input_charset, - self.input_charset) - self.output_codec = CODEC_MAP.get(self.output_charset, - self.output_charset) - - def __str__(self): - return self.input_charset.lower() - - __repr__ = __str__ - - def __eq__(self, other): - return str(self) == str(other).lower() - - def __ne__(self, other): - return not self.__eq__(other) - - def get_body_encoding(self): - """Return the content-transfer-encoding used for body encoding. - - This is either the string `quoted-printable' or `base64' depending on - the encoding used, or it is a function in which case you should call - the function with a single argument, the Message object being - encoded. The function should then set the Content-Transfer-Encoding - header itself to whatever is appropriate. - - Returns "quoted-printable" if self.body_encoding is QP. - Returns "base64" if self.body_encoding is BASE64. - Returns "7bit" otherwise. - """ - assert self.body_encoding != SHORTEST - if self.body_encoding == QP: - return 'quoted-printable' - elif self.body_encoding == BASE64: - return 'base64' - else: - return encode_7or8bit - - def convert(self, s): - """Convert a string from the input_codec to the output_codec.""" - if self.input_codec != self.output_codec: - return str(s, self.input_codec).encode(self.output_codec) - else: - return s - - def to_splittable(self, s): - """Convert a possibly multibyte string to a safely splittable format. - - Uses the input_codec to try and convert the string to Unicode, so it - can be safely split on character boundaries (even for multibyte - characters). - - Returns the string as-is if it isn't known how to convert it to - Unicode with the input_charset. - - Characters that could not be converted to Unicode will be replaced - with the Unicode replacement character U+FFFD. - """ - if isinstance(s, str) or self.input_codec is None: - return s - try: - return str(s, self.input_codec, 'replace') - except LookupError: - # Input codec not installed on system, so return the original - # string unchanged. - return s - - def from_splittable(self, ustr, to_output=True): - """Convert a splittable string back into an encoded string. - - Uses the proper codec to try and convert the string from Unicode back - into an encoded format. Return the string as-is if it is not Unicode, - or if it could not be converted from Unicode. - - Characters that could not be converted from Unicode will be replaced - with an appropriate character (usually '?'). - - If to_output is True (the default), uses output_codec to convert to an - encoded format. If to_output is False, uses input_codec. - """ - if to_output: - codec = self.output_codec - else: - codec = self.input_codec - if not isinstance(ustr, str) or codec is None: - return ustr - try: - return ustr.encode(codec, 'replace') - except LookupError: - # Output codec not installed - return ustr - - def get_output_charset(self): - """Return the output character set. - - This is self.output_charset if that is not None, otherwise it is - self.input_charset. - """ - return self.output_charset or self.input_charset - - def encoded_header_len(self, s): - """Return the length of the encoded header string.""" - cset = self.get_output_charset() - # The len(s) of a 7bit encoding is len(s) - if self.header_encoding == BASE64: - return bongo.external.email.base64mime.base64_len(s) + len(cset) + MISC_LEN - elif self.header_encoding == QP: - return bongo.external.email.quoprimime.header_quopri_len(s) + len(cset) + MISC_LEN - elif self.header_encoding == SHORTEST: - lenb64 = bongo.external.email.base64mime.base64_len(s) - lenqp = bongo.external.email.quoprimime.header_quopri_len(s) - return min(lenb64, lenqp) + len(cset) + MISC_LEN - else: - return len(s) - - def header_encode(self, s, convert=False): - """Header-encode a string, optionally converting it to output_charset. - - If convert is True, the string will be converted from the input - charset to the output charset automatically. This is not useful for - multibyte character sets, which have line length issues (multibyte - characters must be split on a character, not a byte boundary); use the - high-level Header class to deal with these issues. convert defaults - to False. - - The type of encoding (base64 or quoted-printable) will be based on - self.header_encoding. - """ - cset = self.get_output_charset() - if convert: - s = self.convert(s) - # 7bit/8bit encodings return the string unchanged (modulo conversions) - if self.header_encoding == BASE64: - return bongo.external.email.base64mime.header_encode(s, cset) - elif self.header_encoding == QP: - return bongo.external.email.quoprimime.header_encode(s, cset, maxlinelen=None) - elif self.header_encoding == SHORTEST: - lenb64 = bongo.external.email.base64mime.base64_len(s) - lenqp = bongo.external.email.quoprimime.header_quopri_len(s) - if lenb64 < lenqp: - return bongo.external.email.base64mime.header_encode(s, cset) - else: - return bongo.external.email.quoprimime.header_encode(s, cset, maxlinelen=None) - else: - return s - - def body_encode(self, s, convert=True): - """Body-encode a string and convert it to output_charset. - - If convert is True (the default), the string will be converted from - the input charset to output charset automatically. Unlike - header_encode(), there are no issues with byte boundaries and - multibyte charsets in email bodies, so this is usually pretty safe. - - The type of encoding (base64 or quoted-printable) will be based on - self.body_encoding. - """ - if convert: - s = self.convert(s) - # 7bit/8bit encodings return the string unchanged (module conversions) - if self.body_encoding is BASE64: - return bongo.external.email.base64mime.body_encode(s) - elif self.body_encoding is QP: - return bongo.external.email.quoprimime.body_encode(s) - else: - return s diff --git a/src/libs/python/bongo/external/email/encoders.py b/src/libs/python/bongo/external/email/encoders.py deleted file mode 100644 index 06016cd..0000000 --- a/src/libs/python/bongo/external/email/encoders.py +++ /dev/null @@ -1,88 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Barry Warsaw -# Contact: email-sig@python.org - -"""Encodings and related functions.""" - -__all__ = [ - 'encode_7or8bit', - 'encode_base64', - 'encode_noop', - 'encode_quopri', - ] - -import base64 - -from quopri import encodestring as _encodestring - - - -def _qencode(s): - enc = _encodestring(s, quotetabs=True) - # Must encode spaces, which quopri.encodestring() doesn't do - return enc.replace(' ', '=20') - - -def _bencode(s): - # We can't quite use base64.encodestring() since it tacks on a "courtesy - # newline". Blech! - if not s: - return s - hasnewline = (s[-1] == '\n') - value = base64.encodestring(s) - if not hasnewline and value[-1] == '\n': - return value[:-1] - return value - - - -def encode_base64(msg): - """Encode the message's payload in Base64. - - Also, add an appropriate Content-Transfer-Encoding header. - """ - orig = msg.get_payload() - encdata = _bencode(orig) - msg.set_payload(encdata) - msg['Content-Transfer-Encoding'] = 'base64' - - - -def encode_quopri(msg): - """Encode the message's payload in quoted-printable. - - Also, add an appropriate Content-Transfer-Encoding header. - """ - orig = msg.get_payload() - encdata = _qencode(orig) - msg.set_payload(encdata) - msg['Content-Transfer-Encoding'] = 'quoted-printable' - - - -def encode_7or8bit(msg): - """Set the Content-Transfer-Encoding header to 7bit or 8bit.""" - orig = msg.get_payload() - if orig is None: - # There's no payload. For backwards compatibility we use 7bit - msg['Content-Transfer-Encoding'] = '7bit' - return - # We play a trick to make this go fast. If encoding to ASCII succeeds, we - # know the data must be 7bit, otherwise treat it as 8bit. - try: - orig.encode('ascii') - except UnicodeError: - # iso-2022-* is non-ASCII but still 7-bit - charset = msg.get_charset() - output_cset = charset and charset.output_charset - if output_cset and output_cset.lower().startswith('iso-2202-'): - msg['Content-Transfer-Encoding'] = '7bit' - else: - msg['Content-Transfer-Encoding'] = '8bit' - else: - msg['Content-Transfer-Encoding'] = '7bit' - - - -def encode_noop(msg): - """Do nothing.""" diff --git a/src/libs/python/bongo/external/email/errors.py b/src/libs/python/bongo/external/email/errors.py deleted file mode 100644 index d52a624..0000000 --- a/src/libs/python/bongo/external/email/errors.py +++ /dev/null @@ -1,57 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Barry Warsaw -# Contact: email-sig@python.org - -"""email package exception classes.""" - - - -class MessageError(Exception): - """Base class for errors in the email package.""" - - -class MessageParseError(MessageError): - """Base class for message parsing errors.""" - - -class HeaderParseError(MessageParseError): - """Error while parsing headers.""" - - -class BoundaryError(MessageParseError): - """Couldn't find terminating boundary.""" - - -class MultipartConversionError(MessageError, TypeError): - """Conversion to a multipart is prohibited.""" - - -class CharsetError(MessageError): - """An illegal charset was given.""" - - - -# These are parsing defects which the parser was able to work around. -class MessageDefect: - """Base class for a message defect.""" - - def __init__(self, line=None): - self.line = line - -class NoBoundaryInMultipartDefect(MessageDefect): - """A message claimed to be a multipart but had no boundary parameter.""" - -class StartBoundaryNotFoundDefect(MessageDefect): - """The claimed start boundary was never found.""" - -class FirstHeaderLineIsContinuationDefect(MessageDefect): - """A message had a continuation line as its first header line.""" - -class MisplacedEnvelopeHeaderDefect(MessageDefect): - """A 'Unix-from' header was found in the middle of a header block.""" - -class MalformedHeaderDefect(MessageDefect): - """Found a header that was missing a colon, or was otherwise malformed.""" - -class MultipartInvariantViolationDefect(MessageDefect): - """A message claimed to be a multipart but no subparts were found.""" diff --git a/src/libs/python/bongo/external/email/feedparser.py b/src/libs/python/bongo/external/email/feedparser.py deleted file mode 100644 index 22e8211..0000000 --- a/src/libs/python/bongo/external/email/feedparser.py +++ /dev/null @@ -1,479 +0,0 @@ -# Copyright (C) 2004-2006 Python Software Foundation -# Authors: Baxter, Wouters and Warsaw -# Contact: email-sig@python.org - -"""FeedParser - An email feed parser. - -The feed parser implements an interface for incrementally parsing an email -message, line by line. This has advantages for certain applications, such as -those reading email messages off a socket. - -FeedParser.feed() is the primary interface for pushing new data into the -parser. It returns when there's nothing more it can do with the available -data. When you have no more data to push into the parser, call .close(). -This completes the parsing and returns the root message object. - -The other advantage of this parser is that it will never throw a parsing -exception. Instead, when it finds something unexpected, it adds a 'defect' to -the current message. Defects are just instances that live on the message -object's .defects attribute. -""" - -__all__ = ['FeedParser'] - -import re - -from bongo.external.email import errors -from bongo.external.email import message -from bongo.external.email import payloads - -NLCRE = re.compile('\r\n|\r|\n') -NLCRE_bol = re.compile('(\r\n|\r|\n)') -NLCRE_eol = re.compile('(\r\n|\r|\n)$') -NLCRE_crack = re.compile('(\r\n|\r|\n)') -# RFC 2822 $3.6.8 Optional fields. ftext is %d33-57 / %d59-126, Any character -# except controls, SP, and ":". -headerRE = re.compile(r'^(From |[\041-\071\073-\176]{1,}:|[\t ])') -EMPTYSTRING = '' -NL = '\r\n' - -NeedMoreData = object() - - - -class BufferedSubFile(object): - """A file-ish object that can have new data loaded into it. - - You can also push and pop line-matching predicates onto a stack. When the - current predicate matches the current line, a false EOF response - (i.e. empty string) is returned instead. This lets the parser adhere to a - simple abstraction -- it parses until EOF closes the current message. - """ - def __init__(self): - # The last partial line pushed into this object. - self._partial = '' - # The list of full, pushed lines, in reverse order - self._lines = [] - # The stack of false-EOF checking predicates. - self._eofstack = [] - # A flag indicating whether the file has been closed or not. - self._closed = False - - def push_eof_matcher(self, pred): - self._eofstack.append(pred) - - def pop_eof_matcher(self): - return self._eofstack.pop() - - def close(self): - # Don't forget any trailing partial line. - self._lines.append(self._partial) - self._partial = '' - self._closed = True - - def readline(self): - if not self._lines: - if self._closed: - return '' - return NeedMoreData - # Pop the line off the stack and see if it matches the current - # false-EOF predicate. - line = self._lines.pop() - # RFC 2046, section 5.1.2 requires us to recognize outer level - # boundaries at any level of inner nesting. Do this, but be sure it's - # in the order of most to least nested. - for ateof in self._eofstack[::-1]: - if ateof(line): - # We're at the false EOF. But push the last line back first. - self._lines.append(line) - return '' - return line - - def unreadline(self, line): - # Let the consumer push a line back into the buffer. - assert line is not NeedMoreData - self._lines.append(line) - - def push(self, data): - """Push some new data into this object.""" - # Handle any previous leftovers - data, self._partial = self._partial + data, '' - # Crack into lines, but preserve the newlines on the end of each - parts = NLCRE_crack.split(data) - # The *ahem* interesting behaviour of re.split when supplied grouping - # parentheses is that the last element of the resulting list is the - # data after the final RE. In the case of a NL/CR terminated string, - # this is the empty string. - self._partial = parts.pop() - # parts is a list of strings, alternating between the line contents - # and the eol character(s). Gather up a list of lines after - # re-attaching the newlines. - lines = [] - for i in range(len(parts) // 2): - lines.append(parts[i*2] + parts[i*2+1]) - self.pushlines(lines) - - def pushlines(self, lines): - # Reverse and insert at the front of the lines. - self._lines[:0] = lines[::-1] - - def is_closed(self): - return self._closed - - def __iter__(self): - return self - - def __next__(self): - line = self.readline() - if line == '': - raise StopIteration - return line - - - -class FeedParser: - """A feed-style parser of email.""" - - def __init__(self, _factory=message.Message, _payloadfactory=payloads.MemoryPayload): - """_factory is called with no arguments to create a new message obj""" - self._factory = _factory - self._payloadfactory = _payloadfactory - self._input = BufferedSubFile() - self._msgstack = [] - self._parse = self._parsegen().__next__ - self._cur = None - self._last = None - self._headersonly = False - - # Non-public interface for supporting Parser's headersonly flag - def _set_headersonly(self): - self._headersonly = True - - def feed(self, data): - """Push more data into the parser.""" - self._input.push(data) - self._call_parse() - - def _call_parse(self): - try: - self._parse() - except StopIteration: - pass - - def close(self): - """Parse all remaining data and return the root message object.""" - self._input.close() - self._call_parse() - root = self._pop_message() - assert not self._msgstack - # Look for final set of defects - if root.get_content_maintype() == 'multipart' \ - and not root.is_multipart(): - root.defects.append(errors.MultipartInvariantViolationDefect()) - return root - - def _new_message(self): - msg = self._factory() - if self._cur and self._cur.get_content_type() == 'multipart/digest': - msg.set_default_type('message/rfc822') - if self._msgstack: - self._msgstack[-1].attach(msg) - self._msgstack.append(msg) - self._cur = msg - self._last = msg - - def _pop_message(self): - retval = self._msgstack.pop() - if self._msgstack: - self._cur = self._msgstack[-1] - else: - self._cur = None - return retval - - def _parsegen(self): - # Create a new message and start by parsing headers. - self._new_message() - headers = [] - # Collect the headers, searching for a line that doesn't match the RFC - # 2822 header or continuation pattern (including an empty line). - for line in self._input: - if line is NeedMoreData: - yield NeedMoreData - continue - if not headerRE.match(line): - # If we saw the RFC defined header/body separator - # (i.e. newline), just throw it away. Otherwise the line is - # part of the body so push it back. - if not NLCRE.match(line): - self._input.unreadline(line) - break - headers.append(line) - # Done with the headers, so parse them and figure out what we're - # supposed to see in the body of the message. - self._parse_headers(headers) - # Headers-only parsing is a backwards compatibility hack, which was - # necessary in the older parser, which could throw errors. All - # remaining lines in the input are thrown into the message body. - if self._headersonly: - payload = self._payloadfactory(self._cur["Content-Transfer-Encoding"]) - while True: - line = self._input.readline() - if line is NeedMoreData: - yield NeedMoreData - continue - if line == '': - break - payload.add(line) - self._cur.set_payload(payload) - return - if self._cur.get_content_type() == 'message/delivery-status': - # message/delivery-status contains blocks of headers separated by - # a blank line. We'll represent each header block as a separate - # nested message object, but the processing is a bit different - # than standard message/* types because there is no body for the - # nested messages. A blank line separates the subparts. - while True: - self._input.push_eof_matcher(NLCRE.match) - for retval in self._parsegen(): - if retval is NeedMoreData: - yield NeedMoreData - continue - break - msg = self._pop_message() - # We need to pop the EOF matcher in order to tell if we're at - # the end of the current file, not the end of the last block - # of message headers. - self._input.pop_eof_matcher() - # The input stream must be sitting at the newline or at the - # EOF. We want to see if we're at the end of this subpart, so - # first consume the blank line, then test the next line to see - # if we're at this subpart's EOF. - while True: - line = self._input.readline() - if line is NeedMoreData: - yield NeedMoreData - continue - break - while True: - line = self._input.readline() - if line is NeedMoreData: - yield NeedMoreData - continue - break - if line == '': - break - # Not at EOF so this is a line we're going to need. - self._input.unreadline(line) - return - if self._cur.get_content_maintype() == 'message': - # The message claims to be a message/* type, then what follows is - # another RFC 2822 message. - for retval in self._parsegen(): - if retval is NeedMoreData: - yield NeedMoreData - continue - break - self._pop_message() - return - if self._cur.get_content_maintype() == 'multipart': - boundary = self._cur.get_boundary() - if boundary is None: - # The message /claims/ to be a multipart but it has not - # defined a boundary. That's a problem which we'll handle by - # reading everything until the EOF and marking the message as - # defective. - self._cur.defects.append(errors.NoBoundaryInMultipartDefect()) - payload = self._payloadfactory(self._cur["Content-Transfer-Encoding"]) - for line in self._input: - if line is NeedMoreData: - yield NeedMoreData - continue - payload.add(line) - self._cur.set_payload(payload) - return - # Create a line match predicate which matches the inter-part - # boundary as well as the end-of-multipart boundary. Don't push - # this onto the input stream until we've scanned past the - # preamble. - separator = '--' + boundary - boundaryre = re.compile( - '(?P' + re.escape(separator) + - r')(?P--)?(?P[ \t]*)(?P\r\n|\r|\n)?$') - capturing_preamble = True - preamble = [] - linesep = False - while True: - line = self._input.readline() - if line is NeedMoreData: - yield NeedMoreData - continue - if line == '': - break - mo = boundaryre.match(line) - if mo: - # If we're looking at the end boundary, we're done with - # this multipart. If there was a newline at the end of - # the closing boundary, then we need to initialize the - # epilogue with the empty string (see below). - if mo.group('end'): - linesep = mo.group('linesep') - break - # We saw an inter-part boundary. Were we in the preamble? - if capturing_preamble: - if preamble: - # According to RFC 2046, the last newline belongs - # to the boundary. - lastline = preamble[-1] - eolmo = NLCRE_eol.search(lastline) - if eolmo: - preamble[-1] = lastline[:-len(eolmo.group(0))] - self._cur.preamble = EMPTYSTRING.join(preamble) - capturing_preamble = False - self._input.unreadline(line) - continue - # We saw a boundary separating two parts. Consume any - # multiple boundary lines that may be following. Our - # interpretation of RFC 2046 BNF grammar does not produce - # body parts within such double boundaries. - while True: - line = self._input.readline() - if line is NeedMoreData: - yield NeedMoreData - continue - mo = boundaryre.match(line) - if not mo: - self._input.unreadline(line) - break - # Recurse to parse this subpart; the input stream points - # at the subpart's first line. - self._input.push_eof_matcher(boundaryre.match) - for retval in self._parsegen(): - if retval is NeedMoreData: - yield NeedMoreData - continue - break - # Because of RFC 2046, the newline preceding the boundary - # separator actually belongs to the boundary, not the - # previous subpart's payload (or epilogue if the previous - # part is a multipart). - if self._last.get_content_maintype() == 'multipart': - epilogue = self._last.epilogue - if epilogue == '': - self._last.epilogue = None - elif epilogue is not None: - mo = NLCRE_eol.search(epilogue) - if mo: - end = len(mo.group(0)) - self._last.epilogue = epilogue[:-end] - else: - # FIXME - payload = self._last.get_payload_obj() - payload.strip_eol() - self._input.pop_eof_matcher() - self._pop_message() - # Set the multipart up for newline cleansing, which will - # happen if we're in a nested multipart. - self._last = self._cur - else: - # I think we must be in the preamble - assert capturing_preamble - preamble.append(line) - # We've seen either the EOF or the end boundary. If we're still - # capturing the preamble, we never saw the start boundary. Note - # that as a defect and store the captured text as the payload. - # Everything from here to the EOF is epilogue. - if capturing_preamble: - self._cur.defects.append(errors.StartBoundaryNotFoundDefect()) - self._cur.set_payload(EMPTYSTRING.join(preamble)) - epilogue = [] - for line in self._input: - if line is NeedMoreData: - yield NeedMoreData - continue - self._cur.epilogue = EMPTYSTRING.join(epilogue) - return - # If the end boundary ended in a newline, we'll need to make sure - # the epilogue isn't None - if linesep: - epilogue = [''] - else: - epilogue = [] - for line in self._input: - if line is NeedMoreData: - yield NeedMoreData - continue - epilogue.append(line) - # Any CRLF at the front of the epilogue is not technically part of - # the epilogue. Also, watch out for an empty string epilogue, - # which means a single newline. - if epilogue: - firstline = epilogue[0] - bolmo = NLCRE_bol.match(firstline) - if bolmo: - epilogue[0] = firstline[len(bolmo.group(0)):] - self._cur.epilogue = EMPTYSTRING.join(epilogue) - return - # Otherwise, it's some non-multipart type, so the entire rest of the - # file contents becomes the payload. - payload = self._payloadfactory(self._cur["Content-Transfer-Encoding"]) - for line in self._input: - if line is NeedMoreData: - yield NeedMoreData - continue - payload.add(line) - self._cur.set_payload(payload) - - def _parse_headers(self, lines): - # Passed a list of lines that make up the headers for the current msg - lastheader = '' - lastvalue = [] - for lineno, line in enumerate(lines): - # Check for continuation - if line[0] in ' \t': - if not lastheader: - # The first line of the headers was a continuation. This - # is illegal, so let's note the defect, store the illegal - # line, and ignore it for purposes of headers. - defect = errors.FirstHeaderLineIsContinuationDefect(line) - self._cur.defects.append(defect) - continue - lastvalue.append(line) - continue - if lastheader: - # XXX reconsider the joining of folded lines - lhdr = EMPTYSTRING.join(lastvalue)[:-1].rstrip('\r\n') - self._cur[lastheader] = lhdr - lastheader, lastvalue = '', [] - # Check for envelope header, i.e. unix-from - if line.startswith('From '): - if lineno == 0: - # Strip off the trailing newline - mo = NLCRE_eol.search(line) - if mo: - line = line[:-len(mo.group(0))] - self._cur.set_unixfrom(line) - continue - elif lineno == len(lines) - 1: - # Something looking like a unix-from at the end - it's - # probably the first line of the body, so push back the - # line and stop. - self._input.unreadline(line) - return - else: - # Weirdly placed unix-from line. Note this as a defect - # and ignore it. - defect = errors.MisplacedEnvelopeHeaderDefect(line) - self._cur.defects.append(defect) - continue - # Split the line on the colon separating field name from value. - i = line.find(':') - if i < 0: - defect = errors.MalformedHeaderDefect(line) - self._cur.defects.append(defect) - continue - lastheader = line[:i] - lastvalue = [line[i+1:].lstrip()] - # Done with all the lines, so handle the last header. - if lastheader: - # XXX reconsider the joining of folded lines - self._cur[lastheader] = EMPTYSTRING.join(lastvalue).rstrip('\r\n') diff --git a/src/libs/python/bongo/external/email/filegenerator.py b/src/libs/python/bongo/external/email/filegenerator.py deleted file mode 100644 index 1e5a43e..0000000 --- a/src/libs/python/bongo/external/email/filegenerator.py +++ /dev/null @@ -1,203 +0,0 @@ -__all__ = ['FileGenerator'] - -import re -import sys -import time -import random -import warnings - -from bongo.external.email.header import Header -from bongo.external.email.payloads import Payload -import bongo.external.email.generator - -from io import StringIO -UNDERSCORE = '_' -NL = '\n' - -fcre = re.compile(r'^From ', re.MULTILINE) - -def _is8bitstring(s): - if isinstance(s, str): - try: - str(s, 'us-ascii') - except UnicodeError: - return True - return False - -class FileGenerator : - def __init__(self, outfp, manglefrom, maxheaderlen=78) : - self._fp = outfp - self._maxheaderlen = maxheaderlen - self._manglefrom = manglefrom - self._extra = None - - def clone(self, fp) : - return self.__class__(fp, self._manglefrom, self._maxheaderlen) - - def flatten(self, msg, unixfrom=False) : - if unixfrom : - ufrom = msg.get_unixfrom() - if not ufrom : - ufrom = 'From nobody ' + time.ctime(time.time()) - print(ufrom, file=self._fp) - self._write(msg) - - def write(self, buf, manglefrom=True) : - if self._extra : - buf = self._extra + buf - self._extra = None - - if manglefrom and self._manglefrom : - buf = fcre.sub('>From ', buf) - - if len(buf) > 4 : - extra = buf[-4:] - if extra != 'From' : - self._extra = extra - buf = buf[:-4] - - self._fp.write(buf) - - def flush(self) : - if self._extra : - self._fp.write(self._extra) - self._extra = None - - def _write(self, msg) : - self._write_headers(msg) - self._dispatch(msg) - - def _dispatch(self, msg): - # Get the Content-Type: for the message, then try to dispatch to - # self._handle__(). If there's no handler for the - # full MIME type, then dispatch to self._handle_(). If - # that's missing too, then dispatch to self._writeBody(). - main = msg.get_content_maintype() - sub = msg.get_content_subtype() - specific = UNDERSCORE.join((main, sub)).replace('-', '_') - meth = getattr(self, '_handle_' + specific, None) - if meth is None: - generic = main.replace('-', '_') - meth = getattr(self, '_handle_' + generic, None) - if meth is None: - meth = self._writeBody - meth(msg) - self.flush() - - # - # Default handlers - # - - def _write_headers(self, msg, trailing_newline=True): - self.flush() - if msg.get_content_maintype() == "multipart" : - boundary = msg.get_boundary() - if not boundary : - msg.set_boundary(bongo.external.email.generator._make_boundary()) - - for h, v in list(msg.items()): - print('%s:' % h, end=' ', file=self._fp) - if self._maxheaderlen == 0: - # Explicit no-wrapping - print(v, file=self._fp) - elif isinstance(v, Header): - # Header instances know what to do - print(v.encode(), file=self._fp) - elif _is8bitstring(v): - # If we have raw 8bit data in a byte string, we have no idea - # what the encoding is. There is no safe way to split this - # string. If it's ascii-subset, then we could do a normal - # ascii split, but if it's multibyte then we could break the - # string. There's no way to know so the least harm seems to - # be to not split the string and risk it being too long. - print(v, file=self._fp) - else: - # Header's got lots of smarts, so use it. - print(Header( - v, maxlinelen=self._maxheaderlen, - header_name=h, continuation_ws='\t').encode(), file=self._fp) - # A blank line always separates headers from body - if trailing_newline : - print(file=self._fp) - - # - # Handlers for writing types and subtypes - # - - def _handle_text(self, msg): - for buf in msg.iter_payload() : - self.write(buf, True) - - # Default body handler - _writeBody = _handle_text - - def _handle_multipart(self, msg): - subparts = msg.get_payload_obj() - - boundary = msg.get_boundary(failobj=bongo.external.email.generator._make_boundary()) - - if subparts is None : - subparts = [] - elif isinstance(subparts, Payload) : - self._handle_text(msg) - return - - # FIXME: fix bad boundaries - start = self._fp.tell() - - self.flush() - if msg.preamble is not None : - print(msg.preamble, file=self._fp) - - print('--' + boundary, file=self._fp) - - if isinstance(subparts, list) : - first = True - for part in subparts : - if not first: - self.flush() - print(NL + '--' + boundary, file=self._fp) - first = False - - g = self.clone(self._fp) - g.flatten(part) - self.write(NL + '--' + boundary + '--') - - if msg.epilogue is not None : - self.flush() - print(file=self._fp) - self.write(msg.epilogue, True) - - def _strip_newline(self) : - pass - - def _handle_message_delivery_status(self, msg): - # We can't just write the headers directly to self's file object - # because this will leave an extra newline between the last header - # block and the boundary. Sigh. - blocks = [] - for part in msg.get_payload(): - s = StringIO() - g = self.clone(s) - g.flatten(part, unixfrom=False) - text = s.getvalue() - lines = text.split('\n') - # Strip off the unnecessary trailing empty line - if lines and lines[-1] == '': - blocks.append(NL.join(lines[:-1])) - else: - blocks.append(text) - # Now join all the blocks with an empty line. This has the lovely - # effect of separating each block with an empty line, but not adding - # an extra one after the last one. - self.write(NL.join(blocks), True) - - def _handle_message(self, msg): - g = self.clone(self._fp) - # The payload of a message/rfc822 part should be a multipart sequence - # of length 1. The zeroth element of the list should be the Message - # object for the subpart. Extract that object, stringify it, and - # write it out. - g.flatten(msg.get_payload(0), unixfrom=False) - - diff --git a/src/libs/python/bongo/external/email/generator.py b/src/libs/python/bongo/external/email/generator.py deleted file mode 100644 index a3a0505..0000000 --- a/src/libs/python/bongo/external/email/generator.py +++ /dev/null @@ -1,352 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Barry Warsaw -# Contact: email-sig@python.org - -"""Classes to generate plain text from a message object tree.""" - -__all__ = ['Generator', 'DecodedGenerator'] - -import re -import sys -import time -import random -import warnings - -from io import StringIO -from bongo.external.email.header import Header -from bongo.external.email.filegenerator import FileGenerator - -UNDERSCORE = '_' -NL = '\n' - -fcre = re.compile(r'^From ', re.MULTILINE) - -def _is8bitstring(s): - if isinstance(s, str): - try: - str(s, 'us-ascii') - except UnicodeError: - return True - return False - -class Generator(FileGenerator) : - def __init__(self, outfp, mangle_from_=True, maxheaderlen=78) : - FileGenerator.__init__(self, outfp, mangle_from_, maxheaderlen) - -class OldGenerator: - """Generates output from a Message object tree. - - This basic generator writes the message to the given file object as plain - text. - """ - # - # Public interface - # - - def __init__(self, outfp, mangle_from_=True, maxheaderlen=78): - """Create the generator for message flattening. - - outfp is the output file-like object for writing the message to. It - must have a write() method. - - Optional mangle_from_ is a flag that, when True (the default), escapes - From_ lines in the body of the message by putting a `>' in front of - them. - - Optional maxheaderlen specifies the longest length for a non-continued - header. When a header line is longer (in characters, with tabs - expanded to 8 spaces) than maxheaderlen, the header will split as - defined in the Header class. Set maxheaderlen to zero to disable - header wrapping. The default is 78, as recommended (but not required) - by RFC 2822. - """ - self._fp = outfp - self._mangle_from_ = mangle_from_ - self._maxheaderlen = maxheaderlen - - def write(self, s): - # Just delegate to the file object - self._fp.write(s) - - def flatten(self, msg, unixfrom=False): - """Print the message object tree rooted at msg to the output file - specified when the Generator instance was created. - - unixfrom is a flag that forces the printing of a Unix From_ delimiter - before the first object in the message tree. If the original message - has no From_ delimiter, a `standard' one is crafted. By default, this - is False to inhibit the printing of any From_ delimiter. - - Note that for subobjects, no From_ line is printed. - """ - if unixfrom: - ufrom = msg.get_unixfrom() - if not ufrom: - ufrom = 'From nobody ' + time.ctime(time.time()) - print(ufrom, file=self._fp) - self._write(msg) - - def clone(self, fp): - """Clone this generator with the exact same options.""" - return self.__class__(fp, self._mangle_from_, self._maxheaderlen) - - # - # Protected interface - undocumented ;/ - # - - def _write(self, msg): - # We can't write the headers yet because of the following scenario: - # say a multipart message includes the boundary string somewhere in - # its body. We'd have to calculate the new boundary /before/ we write - # the headers so that we can write the correct Content-Type: - # parameter. - # - # The way we do this, so as to make the _handle_*() methods simpler, - # is to cache any subpart writes into a StringIO. The we write the - # headers and the StringIO contents. That way, subpart handlers can - # Do The Right Thing, and can still modify the Content-Type: header if - # necessary. - oldfp = self._fp - try: - self._fp = sfp = StringIO() - self._dispatch(msg) - finally: - self._fp = oldfp - # Write the headers. First we see if the message object wants to - # handle that itself. If not, we'll do it generically. - meth = getattr(msg, '_write_headers', None) - if meth is None: - self._write_headers(msg) - else: - meth(self) - self._fp.write(sfp.getvalue()) - - def _dispatch(self, msg): - # Get the Content-Type: for the message, then try to dispatch to - # self._handle__(). If there's no handler for the - # full MIME type, then dispatch to self._handle_(). If - # that's missing too, then dispatch to self._writeBody(). - main = msg.get_content_maintype() - sub = msg.get_content_subtype() - specific = UNDERSCORE.join((main, sub)).replace('-', '_') - meth = getattr(self, '_handle_' + specific, None) - if meth is None: - generic = main.replace('-', '_') - meth = getattr(self, '_handle_' + generic, None) - if meth is None: - meth = self._writeBody - meth(msg) - - # - # Default handlers - # - - def _write_headers(self, msg): - for h, v in list(msg.items()): - print('%s:' % h, end=' ', file=self._fp) - if self._maxheaderlen == 0: - # Explicit no-wrapping - print(v, file=self._fp) - elif isinstance(v, Header): - # Header instances know what to do - print(v.encode(), file=self._fp) - elif _is8bitstring(v): - # If we have raw 8bit data in a byte string, we have no idea - # what the encoding is. There is no safe way to split this - # string. If it's ascii-subset, then we could do a normal - # ascii split, but if it's multibyte then we could break the - # string. There's no way to know so the least harm seems to - # be to not split the string and risk it being too long. - print(v, file=self._fp) - else: - # Header's got lots of smarts, so use it. - print(Header( - v, maxlinelen=self._maxheaderlen, - header_name=h, continuation_ws='\t').encode(), file=self._fp) - # A blank line always separates headers from body - print(file=self._fp) - - # - # Handlers for writing types and subtypes - # - - def _handle_text(self, msg): - payload = msg.get_payload() - if payload is None: - return - if not isinstance(payload, str): - raise TypeError('string payload expected: %s' % type(payload)) - if self._mangle_from_: - payload = fcre.sub('>From ', payload) - self._fp.write(payload) - - # Default body handler - _writeBody = _handle_text - - def _handle_multipart(self, msg): - # The trick here is to write out each part separately, merge them all - # together, and then make sure that the boundary we've chosen isn't - # present in the payload. - msgtexts = [] - subparts = msg.get_payload() - if subparts is None: - subparts = [] - elif isinstance(subparts, str): - # e.g. a non-strict parse of a message with no starting boundary. - self._fp.write(subparts) - return - elif not isinstance(subparts, list): - # Scalar payload - subparts = [subparts] - for part in subparts: - s = StringIO() - g = self.clone(s) - g.flatten(part, unixfrom=False) - msgtexts.append(s.getvalue()) - # Now make sure the boundary we've selected doesn't appear in any of - # the message texts. - alltext = NL.join(msgtexts) - # BAW: What about boundaries that are wrapped in double-quotes? - boundary = msg.get_boundary(failobj=_make_boundary(alltext)) - # If we had to calculate a new boundary because the body text - # contained that string, set the new boundary. We don't do it - # unconditionally because, while set_boundary() preserves order, it - # doesn't preserve newlines/continuations in headers. This is no big - # deal in practice, but turns out to be inconvenient for the unittest - # suite. - if msg.get_boundary() != boundary: - msg.set_boundary(boundary) - # If there's a preamble, write it out, with a trailing CRLF - if msg.preamble is not None: - print(msg.preamble, file=self._fp) - # dash-boundary transport-padding CRLF - print('--' + boundary, file=self._fp) - # body-part - if msgtexts: - self._fp.write(msgtexts.pop(0)) - # *encapsulation - # --> delimiter transport-padding - # --> CRLF body-part - for body_part in msgtexts: - # delimiter transport-padding CRLF - print(NL + '--' + boundary, file=self._fp) - # body-part - self._fp.write(body_part) - # close-delimiter transport-padding - self._fp.write(NL + '--' + boundary + '--') - if msg.epilogue is not None: - print(file=self._fp) - self._fp.write(msg.epilogue) - - def _handle_message_delivery_status(self, msg): - # We can't just write the headers directly to self's file object - # because this will leave an extra newline between the last header - # block and the boundary. Sigh. - blocks = [] - for part in msg.get_payload(): - s = StringIO() - g = self.clone(s) - g.flatten(part, unixfrom=False) - text = s.getvalue() - lines = text.split('\n') - # Strip off the unnecessary trailing empty line - if lines and lines[-1] == '': - blocks.append(NL.join(lines[:-1])) - else: - blocks.append(text) - # Now join all the blocks with an empty line. This has the lovely - # effect of separating each block with an empty line, but not adding - # an extra one after the last one. - self._fp.write(NL.join(blocks)) - - def _handle_message(self, msg): - s = StringIO() - g = self.clone(s) - # The payload of a message/rfc822 part should be a multipart sequence - # of length 1. The zeroth element of the list should be the Message - # object for the subpart. Extract that object, stringify it, and - # write it out. - g.flatten(msg.get_payload(0), unixfrom=False) - self._fp.write(s.getvalue()) - - - -_FMT = '[Non-text (%(type)s) part of message omitted, filename %(filename)s]' - -class DecodedGenerator(Generator): - """Generator a text representation of a message. - - Like the Generator base class, except that non-text parts are substituted - with a format string representing the part. - """ - def __init__(self, outfp, mangle_from_=True, maxheaderlen=78, fmt=None): - """Like Generator.__init__() except that an additional optional - argument is allowed. - - Walks through all subparts of a message. If the subpart is of main - type `text', then it prints the decoded payload of the subpart. - - Otherwise, fmt is a format string that is used instead of the message - payload. fmt is expanded with the following keywords (in - %(keyword)s format): - - type : Full MIME type of the non-text part - maintype : Main MIME type of the non-text part - subtype : Sub-MIME type of the non-text part - filename : Filename of the non-text part - description: Description associated with the non-text part - encoding : Content transfer encoding of the non-text part - - The default value for fmt is None, meaning - - [Non-text (%(type)s) part of message omitted, filename %(filename)s] - """ - Generator.__init__(self, outfp, mangle_from_, maxheaderlen) - if fmt is None: - self._fmt = _FMT - else: - self._fmt = fmt - - def _dispatch(self, msg): - self.flush() - for part in msg.walk(): - maintype = part.get_content_maintype() - if maintype == 'text': - print(part.get_payload(decode=True), file=self) - elif maintype == 'multipart': - # Just skip this - pass - else: - print(self._fmt % { - 'type' : part.get_content_type(), - 'maintype' : part.get_content_maintype(), - 'subtype' : part.get_content_subtype(), - 'filename' : part.get_filename('[no filename]'), - 'description': part.get('Content-Description', - '[no description]'), - 'encoding' : part.get('Content-Transfer-Encoding', - '[no encoding]'), - }, file=self) - self.flush() - - -# Helper -_width = len(repr(sys.maxsize-1)) -_fmt = '%%0%dd' % _width - -def _make_boundary(text=None): - # Craft a random boundary. If text is given, ensure that the chosen - # boundary doesn't appear in the text. - token = random.randrange(sys.maxsize) - boundary = ('=' * 15) + (_fmt % token) + '==' - if text is None: - return boundary - b = boundary - counter = 0 - while True: - cre = re.compile('^--' + re.escape(b) + '(--)?$', re.MULTILINE) - if not cre.search(text): - break - b = boundary + '.' + str(counter) - counter += 1 - return b diff --git a/src/libs/python/bongo/external/email/header.py b/src/libs/python/bongo/external/email/header.py deleted file mode 100644 index ce34a31..0000000 --- a/src/libs/python/bongo/external/email/header.py +++ /dev/null @@ -1,502 +0,0 @@ -# Copyright (C) 2002-2006 Python Software Foundation -# Author: Ben Gertzfield, Barry Warsaw -# Contact: email-sig@python.org - -"""Header encoding and decoding functionality.""" - -__all__ = [ - 'Header', - 'decode_header', - 'make_header', - ] - -import re -import binascii - -import bongo.external.email.quoprimime -import bongo.external.email.base64mime - -from bongo.external.email.errors import HeaderParseError -from bongo.external.email.charset import Charset - -NL = '\n' -SPACE = ' ' -USPACE = ' ' -SPACE8 = ' ' * 8 -UEMPTYSTRING = '' - -MAXLINELEN = 76 - -USASCII = Charset('us-ascii') -UTF8 = Charset('utf-8') - -# Match encoded-word strings in the form =?charset?q?Hello_World?= -ecre = re.compile(r''' - =\? # literal =? - (?P[^?]*?) # non-greedy up to the next ? is the charset - \? # literal ? - (?P[qb]) # either a "q" or a "b", case insensitive - \? # literal ? - (?P.*?) # non-greedy up to the next ?= is the encoded string - \?= # literal ?= - ''', re.VERBOSE | re.IGNORECASE) - -# Field name regexp, including trailing colon, but not separating whitespace, -# according to RFC 2822. Character range is from tilde to exclamation mark. -# For use with .match() -fcre = re.compile(r'[\041-\176]+:$') - - - -# Helpers -_max_append = bongo.external.email.quoprimime._max_append - - - -def decode_header(header): - """Decode a message header value without converting charset. - - Returns a list of (decoded_string, charset) pairs containing each of the - decoded parts of the header. Charset is None for non-encoded parts of the - header, otherwise a lower-case string containing the name of the character - set specified in the encoded string. - - An email.Errors.HeaderParseError may be raised when certain decoding error - occurs (e.g. a base64 decoding exception). - """ - # If no encoding, just return the header - header = str(header) - if not ecre.search(header): - return [(header, None)] - decoded = [] - dec = '' - for line in header.splitlines(): - # This line might not have an encoding in it - if not ecre.search(line): - decoded.append((line, None)) - continue - parts = ecre.split(line) - while parts: - unenc = parts.pop(0).strip() - if unenc: - # Should we continue a long line? - if decoded and decoded[-1][1] is None: - decoded[-1] = (decoded[-1][0] + SPACE + unenc, None) - else: - decoded.append((unenc, None)) - if parts: - charset, encoding = [s.lower() for s in parts[0:2]] - encoded = parts[2] - dec = None - if encoding == 'q': - dec = bongo.external.email.quoprimime.header_decode(encoded) - elif encoding == 'b': - try: - dec = bongo.external.email.base64mime.decode(encoded) - except binascii.Error: - # Turn this into a higher level exception. BAW: Right - # now we throw the lower level exception away but - # when/if we get exception chaining, we'll preserve it. - raise HeaderParseError - if dec is None: - dec = encoded - - if decoded and decoded[-1][1] == charset: - decoded[-1] = (decoded[-1][0] + dec, decoded[-1][1]) - else: - decoded.append((dec, charset)) - del parts[0:3] - return decoded - - - -def make_header(decoded_seq, maxlinelen=None, header_name=None, - continuation_ws=' '): - """Create a Header from a sequence of pairs as returned by decode_header() - - decode_header() takes a header value string and returns a sequence of - pairs of the format (decoded_string, charset) where charset is the string - name of the character set. - - This function takes one of those sequence of pairs and returns a Header - instance. Optional maxlinelen, header_name, and continuation_ws are as in - the Header constructor. - """ - h = Header(maxlinelen=maxlinelen, header_name=header_name, - continuation_ws=continuation_ws) - for s, charset in decoded_seq: - # None means us-ascii but we can simply pass it on to h.append() - if charset is not None and not isinstance(charset, Charset): - charset = Charset(charset) - h.append(s, charset) - return h - - - -class Header: - def __init__(self, s=None, charset=None, - maxlinelen=None, header_name=None, - continuation_ws=' ', errors='strict'): - """Create a MIME-compliant header that can contain many character sets. - - Optional s is the initial header value. If None, the initial header - value is not set. You can later append to the header with .append() - method calls. s may be a byte string or a Unicode string, but see the - .append() documentation for semantics. - - Optional charset serves two purposes: it has the same meaning as the - charset argument to the .append() method. It also sets the default - character set for all subsequent .append() calls that omit the charset - argument. If charset is not provided in the constructor, the us-ascii - charset is used both as s's initial charset and as the default for - subsequent .append() calls. - - The maximum line length can be specified explicit via maxlinelen. For - splitting the first line to a shorter value (to account for the field - header which isn't included in s, e.g. `Subject') pass in the name of - the field in header_name. The default maxlinelen is 76. - - continuation_ws must be RFC 2822 compliant folding whitespace (usually - either a space or a hard tab) which will be prepended to continuation - lines. - - errors is passed through to the .append() call. - """ - if charset is None: - charset = USASCII - if not isinstance(charset, Charset): - charset = Charset(charset) - self._charset = charset - self._continuation_ws = continuation_ws - cws_expanded_len = len(continuation_ws.replace('\t', SPACE8)) - # BAW: I believe `chunks' and `maxlinelen' should be non-public. - self._chunks = [] - if s is not None: - self.append(s, charset, errors) - if maxlinelen is None: - maxlinelen = MAXLINELEN - if header_name is None: - # We don't know anything about the field header so the first line - # is the same length as subsequent lines. - self._firstlinelen = maxlinelen - else: - # The first line should be shorter to take into account the field - # header. Also subtract off 2 extra for the colon and space. - self._firstlinelen = maxlinelen - len(header_name) - 2 - # Second and subsequent lines should subtract off the length in - # columns of the continuation whitespace prefix. - self._maxlinelen = maxlinelen - cws_expanded_len - - def __str__(self): - """A synonym for self.encode().""" - return self.encode() - - def __unicode__(self): - """Helper for the built-in unicode function.""" - uchunks = [] - lastcs = None - for s, charset in self._chunks: - # We must preserve spaces between encoded and non-encoded word - # boundaries, which means for us we need to add a space when we go - # from a charset to None/us-ascii, or from None/us-ascii to a - # charset. Only do this for the second and subsequent chunks. - nextcs = charset - if uchunks: - if lastcs not in (None, 'us-ascii'): - if nextcs in (None, 'us-ascii'): - uchunks.append(USPACE) - nextcs = None - elif nextcs not in (None, 'us-ascii'): - uchunks.append(USPACE) - lastcs = nextcs - uchunks.append(str(s, str(charset))) - return UEMPTYSTRING.join(uchunks) - - # Rich comparison operators for equality only. BAW: does it make sense to - # have or explicitly disable <, <=, >, >= operators? - def __eq__(self, other): - # other may be a Header or a string. Both are fine so coerce - # ourselves to a string, swap the args and do another comparison. - return other == self.encode() - - def __ne__(self, other): - return not self == other - - def append(self, s, charset=None, errors='strict'): - """Append a string to the MIME header. - - Optional charset, if given, should be a Charset instance or the name - of a character set (which will be converted to a Charset instance). A - value of None (the default) means that the charset given in the - constructor is used. - - s may be a byte string or a Unicode string. If it is a byte string - (i.e. isinstance(s, str) is true), then charset is the encoding of - that byte string, and a UnicodeError will be raised if the string - cannot be decoded with that charset. If s is a Unicode string, then - charset is a hint specifying the character set of the characters in - the string. In this case, when producing an RFC 2822 compliant header - using RFC 2047 rules, the Unicode string will be encoded using the - following charsets in order: us-ascii, the charset hint, utf-8. The - first character set not to provoke a UnicodeError is used. - - Optional `errors' is passed as the third argument to any unicode() or - ustr.encode() call. - """ - if charset is None: - charset = self._charset - elif not isinstance(charset, Charset): - charset = Charset(charset) - # If the charset is our faux 8bit charset, leave the string unchanged - if charset != '8bit': - # We need to test that the string can be converted to unicode and - # back to a byte string, given the input and output codecs of the - # charset. - if isinstance(s, str): - # Possibly raise UnicodeError if the byte string can't be - # converted to a unicode with the input codec of the charset. - incodec = charset.input_codec or 'us-ascii' - ustr = str(s, incodec, errors) - # Now make sure that the unicode could be converted back to a - # byte string with the output codec, which may be different - # than the iput coded. Still, use the original byte string. - outcodec = charset.output_codec or 'us-ascii' - ustr.encode(outcodec, errors) - elif isinstance(s, str): - # Now we have to be sure the unicode string can be converted - # to a byte string with a reasonable output codec. We want to - # use the byte string in the chunk. - for charset in USASCII, charset, UTF8: - try: - outcodec = charset.output_codec or 'us-ascii' - s = s.encode(outcodec, errors) - break - except UnicodeError: - pass - else: - assert False, 'utf-8 conversion failed' - self._chunks.append((s, charset)) - - def _split(self, s, charset, maxlinelen, splitchars): - # Split up a header safely for use with encode_chunks. - splittable = charset.to_splittable(s) - encoded = charset.from_splittable(splittable, True) - elen = charset.encoded_header_len(encoded) - # If the line's encoded length first, just return it - if elen <= maxlinelen: - return [(encoded, charset)] - # If we have undetermined raw 8bit characters sitting in a byte - # string, we really don't know what the right thing to do is. We - # can't really split it because it might be multibyte data which we - # could break if we split it between pairs. The least harm seems to - # be to not split the header at all, but that means they could go out - # longer than maxlinelen. - if charset == '8bit': - return [(s, charset)] - # BAW: I'm not sure what the right test here is. What we're trying to - # do is be faithful to RFC 2822's recommendation that ($2.2.3): - # - # "Note: Though structured field bodies are defined in such a way that - # folding can take place between many of the lexical tokens (and even - # within some of the lexical tokens), folding SHOULD be limited to - # placing the CRLF at higher-level syntactic breaks." - # - # For now, I can only imagine doing this when the charset is us-ascii, - # although it's possible that other charsets may also benefit from the - # higher-level syntactic breaks. - elif charset == 'us-ascii': - return self._split_ascii(s, charset, maxlinelen, splitchars) - # BAW: should we use encoded? - elif elen == len(s): - # We can split on _maxlinelen boundaries because we know that the - # encoding won't change the size of the string - splitpnt = maxlinelen - first = charset.from_splittable(splittable[:splitpnt], False) - last = charset.from_splittable(splittable[splitpnt:], False) - else: - # Binary search for split point - first, last = _binsplit(splittable, charset, maxlinelen) - # first is of the proper length so just wrap it in the appropriate - # chrome. last must be recursively split. - fsplittable = charset.to_splittable(first) - fencoded = charset.from_splittable(fsplittable, True) - chunk = [(fencoded, charset)] - return chunk + self._split(last, charset, self._maxlinelen, splitchars) - - def _split_ascii(self, s, charset, firstlen, splitchars): - chunks = _split_ascii(s, firstlen, self._maxlinelen, - self._continuation_ws, splitchars) - return list(zip(chunks, [charset]*len(chunks))) - - def _encode_chunks(self, newchunks, maxlinelen): - # MIME-encode a header with many different charsets and/or encodings. - # - # Given a list of pairs (string, charset), return a MIME-encoded - # string suitable for use in a header field. Each pair may have - # different charsets and/or encodings, and the resulting header will - # accurately reflect each setting. - # - # Each encoding can be email.Utils.QP (quoted-printable, for - # ASCII-like character sets like iso-8859-1), email.Utils.BASE64 - # (Base64, for non-ASCII like character sets like KOI8-R and - # iso-2022-jp), or None (no encoding). - # - # Each pair will be represented on a separate line; the resulting - # string will be in the format: - # - # =?charset1?q?Mar=EDa_Gonz=E1lez_Alonso?=\n - # =?charset2?b?SvxyZ2VuIEL2aW5n?=" - chunks = [] - for header, charset in newchunks: - if not header: - continue - if charset is None or charset.header_encoding is None: - s = header - else: - s = charset.header_encode(header) - # Don't add more folding whitespace than necessary - if chunks and chunks[-1].endswith(' '): - extra = '' - else: - extra = ' ' - _max_append(chunks, s, maxlinelen, extra) - joiner = NL + self._continuation_ws - return joiner.join(chunks) - - def encode(self, splitchars=';, '): - """Encode a message header into an RFC-compliant format. - - There are many issues involved in converting a given string for use in - an email header. Only certain character sets are readable in most - email clients, and as header strings can only contain a subset of - 7-bit ASCII, care must be taken to properly convert and encode (with - Base64 or quoted-printable) header strings. In addition, there is a - 75-character length limit on any given encoded header field, so - line-wrapping must be performed, even with double-byte character sets. - - This method will do its best to convert the string to the correct - character set used in email, and encode and line wrap it safely with - the appropriate scheme for that character set. - - If the given charset is not known or an error occurs during - conversion, this function will return the header untouched. - - Optional splitchars is a string containing characters to split long - ASCII lines on, in rough support of RFC 2822's `highest level - syntactic breaks'. This doesn't affect RFC 2047 encoded lines. - """ - newchunks = [] - maxlinelen = self._firstlinelen - lastlen = 0 - for s, charset in self._chunks: - # The first bit of the next chunk should be just long enough to - # fill the next line. Don't forget the space separating the - # encoded words. - targetlen = maxlinelen - lastlen - 1 - if targetlen < charset.encoded_header_len(''): - # Stick it on the next line - targetlen = maxlinelen - newchunks += self._split(s, charset, targetlen, splitchars) - lastchunk, lastcharset = newchunks[-1] - lastlen = lastcharset.encoded_header_len(lastchunk) - return self._encode_chunks(newchunks, maxlinelen) - - - -def _split_ascii(s, firstlen, restlen, continuation_ws, splitchars): - lines = [] - maxlen = firstlen - for line in s.splitlines(): - # Ignore any leading whitespace (i.e. continuation whitespace) already - # on the line, since we'll be adding our own. - line = line.lstrip() - if len(line) < maxlen: - lines.append(line) - maxlen = restlen - continue - # Attempt to split the line at the highest-level syntactic break - # possible. Note that we don't have a lot of smarts about field - # syntax; we just try to break on semi-colons, then commas, then - # whitespace. - for ch in splitchars: - if ch in line: - break - else: - # There's nothing useful to split the line on, not even spaces, so - # just append this line unchanged - lines.append(line) - maxlen = restlen - continue - # Now split the line on the character plus trailing whitespace - cre = re.compile(r'%s\s*' % ch) - if ch in ';,': - eol = ch - else: - eol = '' - joiner = eol + ' ' - joinlen = len(joiner) - wslen = len(continuation_ws.replace('\t', SPACE8)) - this = [] - linelen = 0 - for part in cre.split(line): - curlen = linelen + max(0, len(this)-1) * joinlen - partlen = len(part) - onfirstline = not lines - # We don't want to split after the field name, if we're on the - # first line and the field name is present in the header string. - if ch == ' ' and onfirstline and \ - len(this) == 1 and fcre.match(this[0]): - this.append(part) - linelen += partlen - elif curlen + partlen > maxlen: - if this: - lines.append(joiner.join(this) + eol) - # If this part is longer than maxlen and we aren't already - # splitting on whitespace, try to recursively split this line - # on whitespace. - if partlen > maxlen and ch != ' ': - subl = _split_ascii(part, maxlen, restlen, - continuation_ws, ' ') - lines.extend(subl[:-1]) - this = [subl[-1]] - else: - this = [part] - linelen = wslen + len(this[-1]) - maxlen = restlen - else: - this.append(part) - linelen += partlen - # Put any left over parts on a line by themselves - if this: - lines.append(joiner.join(this)) - return lines - - - -def _binsplit(splittable, charset, maxlinelen): - i = 0 - j = len(splittable) - while i < j: - # Invariants: - # 1. splittable[:k] fits for all k <= i (note that we *assume*, - # at the start, that splittable[:0] fits). - # 2. splittable[:k] does not fit for any k > j (at the start, - # this means we shouldn't look at any k > len(splittable)). - # 3. We don't know about splittable[:k] for k in i+1..j. - # 4. We want to set i to the largest k that fits, with i <= k <= j. - # - m = (i+j+1) >> 1 # ceiling((i+j)/2); i < m <= j - chunk = charset.from_splittable(splittable[:m], True) - chunklen = charset.encoded_header_len(chunk) - if chunklen <= maxlinelen: - # m is acceptable, so is a new lower bound. - i = m - else: - # m is not acceptable, so final i must be < m. - j = m - 1 - # i == j. Invariant #1 implies that splittable[:i] fits, and - # invariant #2 implies that splittable[:i+1] does not fit, so i - # is what we're looking for. - first = charset.from_splittable(splittable[:i], False) - last = charset.from_splittable(splittable[i:], False) - return first, last diff --git a/src/libs/python/bongo/external/email/iterators.py b/src/libs/python/bongo/external/email/iterators.py deleted file mode 100644 index eb4cd67..0000000 --- a/src/libs/python/bongo/external/email/iterators.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Barry Warsaw -# Contact: email-sig@python.org - -"""Various types of useful iterators and generators.""" - -__all__ = [ - 'body_line_iterator', - 'typed_subpart_iterator', - 'walk', - # Do not include _structure() since it's part of the debugging API. - ] - -import sys -from io import StringIO - - - -# This function will become a method of the Message class -def walk(self): - """Walk over the message tree, yielding each subpart. - - The walk is performed in depth-first order. This method is a - generator. - """ - yield self - if self.is_multipart(): - for subpart in self.get_payload(): - for subsubpart in subpart.walk(): - yield subsubpart - - - -# These two functions are imported into the Iterators.py interface module. -def body_line_iterator(msg, decode=False): - """Iterate over the parts, returning string payloads line-by-line. - - Optional decode (default False) is passed through to .get_payload(). - """ - for subpart in msg.walk(): - payload = subpart.get_payload(decode=decode) - if isinstance(payload, str): - for line in StringIO(payload): - yield line - - -def typed_subpart_iterator(msg, maintype='text', subtype=None): - """Iterate over the subparts with a given MIME type. - - Use `maintype' as the main MIME type to match against; this defaults to - "text". Optional `subtype' is the MIME subtype to match against; if - omitted, only the main type is matched. - """ - for subpart in msg.walk(): - if subpart.get_content_maintype() == maintype: - if subtype is None or subpart.get_content_subtype() == subtype: - yield subpart - - - -def _structure(msg, fp=None, level=0, include_default=False): - """A handy debugging aid""" - if fp is None: - fp = sys.stdout - tab = ' ' * (level * 4) - print(tab + msg.get_content_type(), end=' ', file=fp) - if include_default: - print('[%s]' % msg.get_default_type(), file=fp) - else: - print(file=fp) - if msg.is_multipart(): - for subpart in msg.get_payload(): - _structure(subpart, fp, level+1, include_default) diff --git a/src/libs/python/bongo/external/email/message.py b/src/libs/python/bongo/external/email/message.py deleted file mode 100644 index 6bc4a5a..0000000 --- a/src/libs/python/bongo/external/email/message.py +++ /dev/null @@ -1,856 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Barry Warsaw -# Contact: email-sig@python.org - -"""Basic message object for the email package object model.""" - -__all__ = ['Message'] - -import re -from io import StringIO - -# Intrapackage imports -import bongo.external.email.charset -from bongo.external.email import utils -from bongo.external.email import errors -from bongo.external.email import payloads - -SEMISPACE = '; ' - -# Regular expression used to split header parameters. BAW: this may be too -# simple. It isn't strictly RFC 2045 (section 5.1) compliant, but it catches -# most headers found in the wild. We may eventually need a full fledged -# parser eventually. -paramre = re.compile(r'\s*;\s*') -# Regular expression that matches `special' characters in parameters, the -# existance of which force quoting of the parameter value. -tspecials = re.compile(r'[ \(\)<>@,;:\\"/\[\]\?=]') - - - -# Helper functions -def _formatparam(param, value=None, quote=True): - """Convenience function to format and return a key=value pair. - - This will quote the value if needed or if quote is true. - """ - if value is not None and len(value) > 0: - # A tuple is used for RFC 2231 encoded parameter values where items - # are (charset, language, value). charset is a string, not a Charset - # instance. - if isinstance(value, tuple): - # Encode as per RFC 2231 - param += '*' - value = utils.encode_rfc2231(value[2], value[0], value[1]) - # BAW: Please check this. I think that if quote is set it should - # force quoting even if not necessary. - if quote or tspecials.search(value): - return '%s="%s"' % (param, utils.quote(value)) - else: - return '%s=%s' % (param, value) - else: - return param - -def _parseparam(s): - plist = [] - while s[:1] == ';': - s = s[1:] - end = s.find(';') - while end > 0 and s.count('"', 0, end) % 2: - end = s.find(';', end + 1) - if end < 0: - end = len(s) - f = s[:end] - if '=' in f: - i = f.index('=') - f = f[:i].strip().lower() + '=' + f[i+1:].strip() - plist.append(f.strip()) - s = s[end:] - return plist - - -def _unquotevalue(value): - # This is different than utils.collapse_rfc2231_value() because it doesn't - # try to convert the value to a unicode. Message.get_param() and - # Message.get_params() are both currently defined to return the tuple in - # the face of RFC 2231 parameters. - if isinstance(value, tuple): - return value[0], value[1], utils.unquote(value[2]) - else: - return utils.unquote(value) - - - -class Message: - """Basic message object. - - A message object is defined as something that has a bunch of RFC 2822 - headers and a payload. It may optionally have an envelope header - (a.k.a. Unix-From or From_ header). If the message is a container (i.e. a - multipart or a message/rfc822), then the payload is a list of Message - objects, otherwise it is a string. - - Message objects implement part of the `mapping' interface, which assumes - there is exactly one occurrance of the header per message. Some headers - do in fact appear multiple times (e.g. Received) and for those headers, - you must use the explicit API to set or get all the headers. Not all of - the mapping methods are implemented. - """ - def __init__(self): - self._headers = [] - self._unixfrom = None - self._payload = None - self._charset = None - # Defaults for multipart messages - self.preamble = self.epilogue = None - self.defects = [] - # Default content type - self._default_type = 'text/plain' - - def __str__(self): - """Return the entire formatted message as a string. - This includes the headers, body, and envelope header. - """ - return self.as_string(unixfrom=True) - - def as_string(self, unixfrom=False): - """Return the entire formatted message as a string. - Optional `unixfrom' when True, means include the Unix From_ envelope - header. - - This is a convenience method and may not generate the message exactly - as you intend because by default it mangles lines that begin with - "From ". For more flexibility, use the flatten() method of a - Generator instance. - """ - from bongo.external.email.Generator import Generator - fp = StringIO() - g = Generator(fp) - g.flatten(self, unixfrom=unixfrom) - return fp.getvalue() - - def is_multipart(self): - """Return True if the message consists of multiple parts.""" - return isinstance(self._payload, list) - - # - # Unix From_ line - # - def set_unixfrom(self, unixfrom): - self._unixfrom = unixfrom - - def get_unixfrom(self): - return self._unixfrom - - # - # Payload manipulation. - # - def attach(self, payload): - """Add the given payload to the current payload. - - The current payload will always be a list of objects after this method - is called. If you want to set the payload to a scalar object, use - set_payload() instead. - """ - if self._payload is None: - self._payload = [payload] - else: - self._payload.append(payload) - - def get_payload(self, i=None, decode=False): - """Return a reference to the payload. - - The payload will either be a list object or a string. If you mutate - the list object, you modify the message's payload in place. Optional - i returns that index into the payload. - - Optional decode is a flag indicating whether the payload should be - decoded or not, according to the Content-Transfer-Encoding header - (default is False). - - When True and the message is not a multipart, the payload will be - decoded if this header's value is `quoted-printable' or `base64'. If - some other encoding is used, or the header is missing, or if the - payload has bogus data (i.e. bogus base64 or uuencoded data), the - payload is returned as-is. - - If the message is a multipart and the decode flag is True, then None - is returned. - """ - if i is None : - if isinstance(self._payload, list) : - if decode : - return None - else : - return self._payload - elif isinstance(self._payload, str): - if self._charset and not decode: - return self._charset.body_encode(self._payload) - else : - return self._payload - elif isinstance(self._payload, payloads.Payload) : - ret = "" - for item in self.iter_payload(decode) : - ret += item - return ret - elif not isinstance(self._payload, list): - raise TypeError('Expected list, got %s' % type(self._payload)) - else: - # Multipart container - if decode: - return None - else: - return self._payload[i] - - def get_payload_obj(self) : - if self._payload == None : - return None - #XXX: document - if isinstance(self._payload, str): - if self._charset : - charset = self._charset.input_charset - else : - charset = "us-ascii" - - payload = payloads.MemoryPayload(charset, self._payload) - elif isinstance(self._payload, payloads.Payload) or isinstance(self._payload, list) : - payload = self._payload - else : - print("unsupported payload type: %s" % (str(self._payload))) - raise TypeError('unsupported payload type for iterating') - - return payload - - def set_payload(self, payload, charset=None): - """Set the payload to the given value. - - Optional charset sets the message's default character set. See - set_charset() for details. - """ - if isinstance(payload, payloads.Payload): - if payload.encoding: - if 'Content-Transfer-Encoding' in self : - self.replace_header('Content-Transfer-Encoding', payload.encoding) - else : - self['Content-Transfer-Encoding'] = payload.encoding - - self._payload = payload - if charset is not None: - self.set_charset(charset) - - def iter_payload(self, decode=False): - payload = self.get_payload_obj() - if payload is None : - return - if not isinstance(payload, payloads.Payload) : - raise TypeError('unsupported payload type for iterating') - - if self._charset : - charset = self._charset - else : - charset = self._read_charset() - - encoding = self['Content-Transfer-Encoding'] - - encode = not decode and charset and encoding != payload.encoding - subdecode = decode or not charset or (encoding != payload.encoding) - - leftover = None - for buf in payload.iter(subdecode): - if encode : - if isinstance(buf, str) : - buf = buf.encode(charset.get_output_charset()) - - if charset.body_encoding == bongo.external.email.charset.BASE64 : - # Encode 57 bytes at a time (lines 76 long) - if leftover : - buf = leftover + buf - leftover = None - - amountLeftover = len(buf) % 57 - leftover = buf[-amountLeftover:] - buf = buf[:-amountLeftover] - - if buf != '' : - buf = charset.body_encode(buf) - else : - buf = None - - if buf : - yield buf - - if leftover: - leftover = charset.body_encode(leftover) - yield leftover - - def set_charset(self, charset): - """Set the charset of the payload to a given character set. - - charset can be a Charset instance, a string naming a character set, or - None. If it is a string it will be converted to a Charset instance. - If charset is None, the charset parameter will be removed from the - Content-Type field. Anything else will generate a TypeError. - - The message will be assumed to be of type text/* encoded with - charset.input_charset. It will be converted to charset.output_charset - and encoded properly, if needed, when generating the plain text - representation of the message. MIME headers (MIME-Version, - Content-Type, Content-Transfer-Encoding) will be added as needed. - - """ - if charset is None: - self.del_param('charset') - self._charset = None - return - if isinstance(charset, str): - charset = bongo.external.email.charset.Charset(charset) - if not isinstance(charset, bongo.external.email.charset.Charset): - raise TypeError(charset) - # BAW: should we accept strings that can serve as arguments to the - # Charset constructor? - - self._charset = charset - if 'MIME-Version' not in self: - self.add_header('MIME-Version', '1.0') - if 'Content-Type' not in self: - self.add_header('Content-Type', 'text/plain', - charset=charset.get_output_charset()) - else: - self.set_param('charset', charset.get_output_charset()) - - if not self['Content-Transfer-Encoding'] : - cte = charset.get_body_encoding() - try : - cte(self) - except TypeError: - self.add_header('Content-Transfer-Encoding', cte) - - def _read_charset(self) : - charset = self.get_param('charset') or 'us-ascii' - encoding = self['Content-Transfer-Encoding'] - return bongo.external.email.charset.Charset(charset, encoding) - - def get_charset(self): - """Return the Charset instance associated with the message's payload. - """ - return self._charset - - # - # MAPPING INTERFACE (partial) - # - def __len__(self): - """Return the total number of headers, including duplicates.""" - return len(self._headers) - - def __getitem__(self, name): - """Get a header value. - - Return None if the header is missing instead of raising an exception. - - Note that if the header appeared multiple times, exactly which - occurrance gets returned is undefined. Use get_all() to get all - the values matching a header field name. - """ - return self.get(name) - - def __setitem__(self, name, val): - """Set the value of a header. - - Note: this does not overwrite an existing header with the same field - name. Use __delitem__() first to delete any existing headers. - """ - self._headers.append((name, val)) - - def __delitem__(self, name): - """Delete all occurrences of a header, if present. - - Does not raise an exception if the header is missing. - """ - name = name.lower() - newheaders = [] - for k, v in self._headers: - if k.lower() != name: - newheaders.append((k, v)) - self._headers = newheaders - - def __contains__(self, name): - return name.lower() in [k.lower() for k, v in self._headers] - - def has_key(self, name): - """Return true if the message contains the header.""" - missing = object() - return self.get(name, missing) is not missing - - def keys(self): - """Return a list of all the message's header field names. - - These will be sorted in the order they appeared in the original - message, or were added to the message, and may contain duplicates. - Any fields deleted and re-inserted are always appended to the header - list. - """ - return [k for k, v in self._headers] - - def values(self): - """Return a list of all the message's header values. - - These will be sorted in the order they appeared in the original - message, or were added to the message, and may contain duplicates. - Any fields deleted and re-inserted are always appended to the header - list. - """ - return [v for k, v in self._headers] - - def items(self): - """Get all the message's header fields and values. - - These will be sorted in the order they appeared in the original - message, or were added to the message, and may contain duplicates. - Any fields deleted and re-inserted are always appended to the header - list. - """ - return self._headers[:] - - def get(self, name, failobj=None): - """Get a header value. - - Like __getitem__() but return failobj instead of None when the field - is missing. - """ - name = name.lower() - for k, v in self._headers: - if k.lower() == name: - return v - return failobj - - # - # Additional useful stuff - # - - def get_all(self, name, failobj=None): - """Return a list of all the values for the named field. - - These will be sorted in the order they appeared in the original - message, and may contain duplicates. Any fields deleted and - re-inserted are always appended to the header list. - - If no such fields exist, failobj is returned (defaults to None). - """ - values = [] - name = name.lower() - for k, v in self._headers: - if k.lower() == name: - values.append(v) - if not values: - return failobj - return values - - def add_header(self, _name, _value, **_params): - """Extended header setting. - - name is the header field to add. keyword arguments can be used to set - additional parameters for the header field, with underscores converted - to dashes. Normally the parameter will be added as key="value" unless - value is None, in which case only the key will be added. - - Example: - - msg.add_header('content-disposition', 'attachment', filename='bud.gif') - """ - parts = [] - for k, v in list(_params.items()): - if v is None: - parts.append(k.replace('_', '-')) - else: - parts.append(_formatparam(k.replace('_', '-'), v)) - if _value is not None: - parts.insert(0, _value) - self._headers.append((_name, SEMISPACE.join(parts))) - - def replace_header(self, _name, _value): - """Replace a header. - - Replace the first matching header found in the message, retaining - header order and case. If no matching header was found, a KeyError is - raised. - """ - _name = _name.lower() - for i, (k, v) in zip(list(range(len(self._headers))), self._headers): - if k.lower() == _name: - self._headers[i] = (k, _value) - break - else: - raise KeyError(_name) - - # - # Use these three methods instead of the three above. - # - - def get_content_type(self): - """Return the message's content type. - - The returned string is coerced to lower case of the form - `maintype/subtype'. If there was no Content-Type header in the - message, the default type as given by get_default_type() will be - returned. Since according to RFC 2045, messages always have a default - type this will always return a value. - - RFC 2045 defines a message's default type to be text/plain unless it - appears inside a multipart/digest container, in which case it would be - message/rfc822. - """ - missing = object() - value = self.get('content-type', missing) - if value is missing: - # This should have no parameters - return self.get_default_type() - ctype = paramre.split(value)[0].lower().strip() - # RFC 2045, section 5.2 says if its invalid, use text/plain - if ctype.count('/') != 1: - return 'text/plain' - return ctype - - def get_content_maintype(self): - """Return the message's main content type. - - This is the `maintype' part of the string returned by - get_content_type(). - """ - ctype = self.get_content_type() - return ctype.split('/')[0] - - def get_content_subtype(self): - """Returns the message's sub-content type. - - This is the `subtype' part of the string returned by - get_content_type(). - """ - ctype = self.get_content_type() - return ctype.split('/')[1] - - def get_default_type(self): - """Return the `default' content type. - - Most messages have a default content type of text/plain, except for - messages that are subparts of multipart/digest containers. Such - subparts have a default content type of message/rfc822. - """ - return self._default_type - - def set_default_type(self, ctype): - """Set the `default' content type. - - ctype should be either "text/plain" or "message/rfc822", although this - is not enforced. The default content type is not stored in the - Content-Type header. - """ - self._default_type = ctype - - def _get_params_preserve(self, failobj, header): - # Like get_params() but preserves the quoting of values. BAW: - # should this be part of the public interface? - missing = object() - value = self.get(header, missing) - if value is missing: - return failobj - params = [] - for p in _parseparam(';' + value): - try: - name, val = p.split('=', 1) - name = name.strip() - val = val.strip() - except ValueError: - # Must have been a bare attribute - name = p.strip() - val = '' - params.append((name, val)) - params = utils.decode_params(params) - return params - - def get_params(self, failobj=None, header='content-type', unquote=True): - """Return the message's Content-Type parameters, as a list. - - The elements of the returned list are 2-tuples of key/value pairs, as - split on the `=' sign. The left hand side of the `=' is the key, - while the right hand side is the value. If there is no `=' sign in - the parameter the value is the empty string. The value is as - described in the get_param() method. - - Optional failobj is the object to return if there is no Content-Type - header. Optional header is the header to search instead of - Content-Type. If unquote is True, the value is unquoted. - """ - missing = object() - params = self._get_params_preserve(missing, header) - if params is missing: - return failobj - if unquote: - return [(k, _unquotevalue(v)) for k, v in params] - else: - return params - - def get_param(self, param, failobj=None, header='content-type', - unquote=True): - """Return the parameter value if found in the Content-Type header. - - Optional failobj is the object to return if there is no Content-Type - header, or the Content-Type header has no such parameter. Optional - header is the header to search instead of Content-Type. - - Parameter keys are always compared case insensitively. The return - value can either be a string, or a 3-tuple if the parameter was RFC - 2231 encoded. When it's a 3-tuple, the elements of the value are of - the form (CHARSET, LANGUAGE, VALUE). Note that both CHARSET and - LANGUAGE can be None, in which case you should consider VALUE to be - encoded in the us-ascii charset. You can usually ignore LANGUAGE. - - Your application should be prepared to deal with 3-tuple return - values, and can convert the parameter to a Unicode string like so: - - param = msg.get_param('foo') - if isinstance(param, tuple): - param = unicode(param[2], param[0] or 'us-ascii') - - In any case, the parameter value (either the returned string, or the - VALUE item in the 3-tuple) is always unquoted, unless unquote is set - to False. - """ - if header not in self: - return failobj - for k, v in self._get_params_preserve(failobj, header): - if k.lower() == param.lower(): - if unquote: - return _unquotevalue(v) - else: - return v - return failobj - - def set_param(self, param, value, header='Content-Type', requote=True, - charset=None, language=''): - """Set a parameter in the Content-Type header. - - If the parameter already exists in the header, its value will be - replaced with the new value. - - If header is Content-Type and has not yet been defined for this - message, it will be set to "text/plain" and the new parameter and - value will be appended as per RFC 2045. - - An alternate header can specified in the header argument, and all - parameters will be quoted as necessary unless requote is False. - - If charset is specified, the parameter will be encoded according to RFC - 2231. Optional language specifies the RFC 2231 language, defaulting - to the empty string. Both charset and language should be strings. - """ - if not isinstance(value, tuple) and charset: - value = (charset, language, value) - - if header not in self and header.lower() == 'content-type': - ctype = 'text/plain' - else: - ctype = self.get(header) - if not self.get_param(param, header=header): - if not ctype: - ctype = _formatparam(param, value, requote) - else: - ctype = SEMISPACE.join( - [ctype, _formatparam(param, value, requote)]) - else: - ctype = '' - for old_param, old_value in self.get_params(header=header, - unquote=requote): - append_param = '' - if old_param.lower() == param.lower(): - append_param = _formatparam(param, value, requote) - else: - append_param = _formatparam(old_param, old_value, requote) - if not ctype: - ctype = append_param - else: - ctype = SEMISPACE.join([ctype, append_param]) - if ctype != self.get(header): - del self[header] - self[header] = ctype - - def del_param(self, param, header='content-type', requote=True): - """Remove the given parameter completely from the Content-Type header. - - The header will be re-written in place without the parameter or its - value. All values will be quoted as necessary unless requote is - False. Optional header specifies an alternative to the Content-Type - header. - """ - if header not in self: - return - new_ctype = '' - for p, v in self.get_params(header=header, unquote=requote): - if p.lower() != param.lower(): - if not new_ctype: - new_ctype = _formatparam(p, v, requote) - else: - new_ctype = SEMISPACE.join([new_ctype, - _formatparam(p, v, requote)]) - if new_ctype != self.get(header): - del self[header] - self[header] = new_ctype - - def set_type(self, type, header='Content-Type', requote=True): - """Set the main type and subtype for the Content-Type header. - - type must be a string in the form "maintype/subtype", otherwise a - ValueError is raised. - - This method replaces the Content-Type header, keeping all the - parameters in place. If requote is False, this leaves the existing - header's quoting as is. Otherwise, the parameters will be quoted (the - default). - - An alternative header can be specified in the header argument. When - the Content-Type header is set, we'll always also add a MIME-Version - header. - """ - # BAW: should we be strict? - if not type.count('/') == 1: - raise ValueError - # Set the Content-Type, you get a MIME-Version - if header.lower() == 'content-type': - del self['mime-version'] - self['MIME-Version'] = '1.0' - if header not in self: - self[header] = type - return - params = self.get_params(header=header, unquote=requote) - del self[header] - self[header] = type - # Skip the first param; it's the old type. - for p, v in params[1:]: - self.set_param(p, v, header, requote) - - def get_filename(self, failobj=None): - """Return the filename associated with the payload if present. - - The filename is extracted from the Content-Disposition header's - `filename' parameter, and it is unquoted. If that header is missing - the `filename' parameter, this method falls back to looking for the - `name' parameter. - """ - - missing = object() - filename = self.get_param('filename', missing, 'content-disposition') - if filename is missing: - filename = self.get_param('name', missing, 'content-disposition') - if filename is missing: - return failobj - ret = utils.collapse_rfc2231_value(filename).strip() - return ret - - def get_boundary(self, failobj=None): - """Return the boundary associated with the payload if present. - - The boundary is extracted from the Content-Type header's `boundary' - parameter, and it is unquoted. - """ - missing = object() - boundary = self.get_param('boundary', missing) - if boundary is missing: - return failobj - # RFC 2046 says that boundaries may begin but not end in w/s - return utils.collapse_rfc2231_value(boundary).rstrip() - - def set_boundary(self, boundary): - """Set the boundary parameter in Content-Type to 'boundary'. - - This is subtly different than deleting the Content-Type header and - adding a new one with a new boundary parameter via add_header(). The - main difference is that using the set_boundary() method preserves the - order of the Content-Type header in the original message. - - HeaderParseError is raised if the message has no Content-Type header. - """ - missing = object() - params = self._get_params_preserve(missing, 'content-type') - if params is missing: - # There was no Content-Type header, and we don't know what type - # to set it to, so raise an exception. - raise errors.HeaderParseError('No Content-Type header found') - newparams = [] - foundp = False - for pk, pv in params: - if pk.lower() == 'boundary': - newparams.append(('boundary', '"%s"' % boundary)) - foundp = True - else: - newparams.append((pk, pv)) - if not foundp: - # The original Content-Type header had no boundary attribute. - # Tack one on the end. BAW: should we raise an exception - # instead??? - newparams.append(('boundary', '"%s"' % boundary)) - # Replace the existing Content-Type header with the new value - newheaders = [] - for h, v in self._headers: - if h.lower() == 'content-type': - parts = [] - for k, v in newparams: - if v == '': - parts.append(k) - else: - parts.append('%s=%s' % (k, v)) - newheaders.append((h, SEMISPACE.join(parts))) - - else: - newheaders.append((h, v)) - self._headers = newheaders - - def get_content_charset(self, failobj=None): - """Return the charset parameter of the Content-Type header. - - The returned string is always coerced to lower case. If there is no - Content-Type header, or if that header has no charset parameter, - failobj is returned. - """ - missing = object() - charset = self.get_param('charset', missing) - if charset is missing: - return failobj - if isinstance(charset, tuple): - # RFC 2231 encoded, so decode it, and it better end up as ascii. - pcharset = charset[0] or 'us-ascii' - - try: - # LookupError will be raised if the charset isn't known to - # Python. UnicodeError will be raised if the encoded text - # contains a character not in the charset. - charset = str(charset[2], pcharset).encode('us-ascii') - except (LookupError, UnicodeError): - charset = charset[2] - # charset character must be in us-ascii range - try: - charset = str(charset, 'us-ascii').encode('us-ascii') - except UnicodeError: - return failobj - - # RFC 2046, $4.1.2 says charsets are not case sensitive - return charset.lower() - - def get_charsets(self, failobj=None): - """Return a list containing the charset(s) used in this message. - - The returned list of items describes the Content-Type headers' - charset parameter for this message and all the subparts in its - payload. - - Each item will either be a string (the value of the charset parameter - in the Content-Type header of that part) or the value of the - 'failobj' parameter (defaults to None), if the part does not have a - main MIME type of "text", or the charset is not defined. - - The list will contain one string for each part of the message, plus - one for the container message (i.e. self), so that a non-multipart - message will still return a list of length 1. - """ - return [part.get_content_charset(failobj) for part in self.walk()] - - # I.e. def walk(self): ... - from bongo.external.email.iterators import walk diff --git a/src/libs/python/bongo/external/email/mime/__init__.py b/src/libs/python/bongo/external/email/mime/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/libs/python/bongo/external/email/mime/application.py b/src/libs/python/bongo/external/email/mime/application.py deleted file mode 100644 index 51bb75c..0000000 --- a/src/libs/python/bongo/external/email/mime/application.py +++ /dev/null @@ -1,36 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Keith Dart -# Contact: email-sig@python.org - -"""Class representing application/* type MIME documents.""" - -__all__ = ["MIMEApplication"] - -from bongo.external.email import encoders -from bongo.external.email.mime.nonmultipart import MIMENonMultipart - - -class MIMEApplication(MIMENonMultipart): - """Class for generating application/* MIME documents.""" - - def __init__(self, _data, _subtype='octet-stream', - _encoder=encoders.encode_base64, **_params): - """Create an application/* type MIME document. - - _data is a string containing the raw applicatoin data. - - _subtype is the MIME content type subtype, defaulting to - 'octet-stream'. - - _encoder is a function which will perform the actual encoding for - transport of the application data, defaulting to base64 encoding. - - Any additional keyword arguments are passed to the base class - constructor, which turns them into parameters on the Content-Type - header. - """ - if _subtype is None: - raise TypeError('Invalid application MIME subtype') - MIMENonMultipart.__init__(self, 'application', _subtype, **_params) - self.set_payload(_data) - _encoder(self) diff --git a/src/libs/python/bongo/external/email/mime/audio.py b/src/libs/python/bongo/external/email/mime/audio.py deleted file mode 100644 index d07a9d4..0000000 --- a/src/libs/python/bongo/external/email/mime/audio.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Anthony Baxter -# Contact: email-sig@python.org - -"""Class representing audio/* type MIME documents.""" - -__all__ = ['MIMEAudio'] - -import sndhdr - -from io import StringIO -from bongo.external.email import encoders -from bongo.external.email.mime.nonmultipart import MIMENonMultipart - - - -_sndhdr_MIMEmap = {'au' : 'basic', - 'wav' :'x-wav', - 'aiff':'x-aiff', - 'aifc':'x-aiff', - } - -# There are others in sndhdr that don't have MIME types. :( -# Additional ones to be added to sndhdr? midi, mp3, realaudio, wma?? -def _whatsnd(data): - """Try to identify a sound file type. - - sndhdr.what() has a pretty cruddy interface, unfortunately. This is why - we re-do it here. It would be easier to reverse engineer the Unix 'file' - command and use the standard 'magic' file, as shipped with a modern Unix. - """ - hdr = data[:512] - fakefile = StringIO(hdr) - for testfn in sndhdr.tests: - res = testfn(hdr, fakefile) - if res is not None: - return _sndhdr_MIMEmap.get(res[0]) - return None - - - -class MIMEAudio(MIMENonMultipart): - """Class for generating audio/* MIME documents.""" - - def __init__(self, _audiodata, _subtype=None, - _encoder=encoders.encode_base64, **_params): - """Create an audio/* type MIME document. - - _audiodata is a string containing the raw audio data. If this data - can be decoded by the standard Python `sndhdr' module, then the - subtype will be automatically included in the Content-Type header. - Otherwise, you can specify the specific audio subtype via the - _subtype parameter. If _subtype is not given, and no subtype can be - guessed, a TypeError is raised. - - _encoder is a function which will perform the actual encoding for - transport of the image data. It takes one argument, which is this - Image instance. It should use get_payload() and set_payload() to - change the payload to the encoded form. It should also add any - Content-Transfer-Encoding or other headers to the message as - necessary. The default encoding is Base64. - - Any additional keyword arguments are passed to the base class - constructor, which turns them into parameters on the Content-Type - header. - """ - if _subtype is None: - _subtype = _whatsnd(_audiodata) - if _subtype is None: - raise TypeError('Could not find audio MIME subtype') - MIMENonMultipart.__init__(self, 'audio', _subtype, **_params) - self.set_payload(_audiodata) - _encoder(self) diff --git a/src/libs/python/bongo/external/email/mime/base.py b/src/libs/python/bongo/external/email/mime/base.py deleted file mode 100644 index 390e25a..0000000 --- a/src/libs/python/bongo/external/email/mime/base.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Barry Warsaw -# Contact: email-sig@python.org - -"""Base class for MIME specializations.""" - -__all__ = ['MIMEBase'] - -from bongo.external.email import message - - - -class MIMEBase(message.Message): - """Base class for MIME specializations.""" - - def __init__(self, _maintype, _subtype, **_params): - """This constructor adds a Content-Type: and a MIME-Version: header. - - The Content-Type: header is taken from the _maintype and _subtype - arguments. Additional parameters for this header are taken from the - keyword arguments. - """ - message.Message.__init__(self) - ctype = '%s/%s' % (_maintype, _subtype) - self.add_header('Content-Type', ctype, **_params) - self['MIME-Version'] = '1.0' diff --git a/src/libs/python/bongo/external/email/mime/image.py b/src/libs/python/bongo/external/email/mime/image.py deleted file mode 100644 index 9e11e3a..0000000 --- a/src/libs/python/bongo/external/email/mime/image.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Barry Warsaw -# Contact: email-sig@python.org - -"""Class representing image/* type MIME documents.""" - -__all__ = ['MIMEImage'] - -import imghdr - -from bongo.external.email import encoders -from bongo.external.email.mime.nonmultipart import MIMENonMultipart - - - -class MIMEImage(MIMENonMultipart): - """Class for generating image/* type MIME documents.""" - - def __init__(self, _imagedata, _subtype=None, - _encoder=encoders.encode_base64, **_params): - """Create an image/* type MIME document. - - _imagedata is a string containing the raw image data. If this data - can be decoded by the standard Python `imghdr' module, then the - subtype will be automatically included in the Content-Type header. - Otherwise, you can specify the specific image subtype via the _subtype - parameter. - - _encoder is a function which will perform the actual encoding for - transport of the image data. It takes one argument, which is this - Image instance. It should use get_payload() and set_payload() to - change the payload to the encoded form. It should also add any - Content-Transfer-Encoding or other headers to the message as - necessary. The default encoding is Base64. - - Any additional keyword arguments are passed to the base class - constructor, which turns them into parameters on the Content-Type - header. - """ - if _subtype is None: - _subtype = imghdr.what(None, _imagedata) - if _subtype is None: - raise TypeError('Could not guess image MIME subtype') - MIMENonMultipart.__init__(self, 'image', _subtype, **_params) - self.set_payload(_imagedata) - _encoder(self) diff --git a/src/libs/python/bongo/external/email/mime/message.py b/src/libs/python/bongo/external/email/mime/message.py deleted file mode 100644 index 22af733..0000000 --- a/src/libs/python/bongo/external/email/mime/message.py +++ /dev/null @@ -1,34 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Barry Warsaw -# Contact: email-sig@python.org - -"""Class representing message/* MIME documents.""" - -__all__ = ['MIMEMessage'] - -from bongo.external.email import message -from bongo.external.email.mime.nonmultipart import MIMENonMultipart - - - -class MIMEMessage(MIMENonMultipart): - """Class representing message/* MIME documents.""" - - def __init__(self, _msg, _subtype='rfc822'): - """Create a message/* type MIME document. - - _msg is a message object and must be an instance of Message, or a - derived class of Message, otherwise a TypeError is raised. - - Optional _subtype defines the subtype of the contained message. The - default is "rfc822" (this is defined by the MIME standard, even though - the term "rfc822" is technically outdated by RFC 2822). - """ - MIMENonMultipart.__init__(self, 'message', _subtype) - if not isinstance(_msg, message.Message): - raise TypeError('Argument is not an instance of Message') - # It's convenient to use this base class method. We need to do it - # this way or we'll get an exception - message.Message.attach(self, _msg) - # And be sure our default type is set correctly - self.set_default_type('message/rfc822') diff --git a/src/libs/python/bongo/external/email/mime/multipart.py b/src/libs/python/bongo/external/email/mime/multipart.py deleted file mode 100644 index c679a57..0000000 --- a/src/libs/python/bongo/external/email/mime/multipart.py +++ /dev/null @@ -1,41 +0,0 @@ -# Copyright (C) 2002-2006 Python Software Foundation -# Author: Barry Warsaw -# Contact: email-sig@python.org - -"""Base class for MIME multipart/* type messages.""" - -__all__ = ['MIMEMultipart'] - -from bongo.external.email.mime.base import MIMEBase - - - -class MIMEMultipart(MIMEBase): - """Base class for MIME multipart/* type messages.""" - - def __init__(self, _subtype='mixed', boundary=None, _subparts=None, - **_params): - """Creates a multipart/* type message. - - By default, creates a multipart/mixed message, with proper - Content-Type and MIME-Version headers. - - _subtype is the subtype of the multipart content type, defaulting to - `mixed'. - - boundary is the multipart boundary string. By default it is - calculated as needed. - - _subparts is a sequence of initial subparts for the payload. It - must be an iterable object, such as a list. You can always - attach new subparts to the message by using the attach() method. - - Additional parameters for the Content-Type header are taken from the - keyword arguments (or passed into the _params argument). - """ - MIMEBase.__init__(self, 'multipart', _subtype, **_params) - if _subparts: - for p in _subparts: - self.attach(p) - if boundary: - self.set_boundary(boundary) diff --git a/src/libs/python/bongo/external/email/mime/nonmultipart.py b/src/libs/python/bongo/external/email/mime/nonmultipart.py deleted file mode 100644 index 025c6b2..0000000 --- a/src/libs/python/bongo/external/email/mime/nonmultipart.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright (C) 2002-2006 Python Software Foundation -# Author: Barry Warsaw -# Contact: email-sig@python.org - -"""Base class for MIME type messages that are not multipart.""" - -__all__ = ['MIMENonMultipart'] - -from bongo.external.email import errors -from bongo.external.email.mime.base import MIMEBase - - - -class MIMENonMultipart(MIMEBase): - """Base class for MIME multipart/* type messages.""" - - __pychecker__ = 'unusednames=payload' - - def attach(self, payload): - # The public API prohibits attaching multiple subparts to MIMEBase - # derived subtypes since none of them are, by definition, of content - # type multipart/* - raise errors.MultipartConversionError( - 'Cannot attach additional subparts to non-multipart/*') - - del __pychecker__ diff --git a/src/libs/python/bongo/external/email/mime/text.py b/src/libs/python/bongo/external/email/mime/text.py deleted file mode 100644 index 716ea8c..0000000 --- a/src/libs/python/bongo/external/email/mime/text.py +++ /dev/null @@ -1,30 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Barry Warsaw -# Contact: email-sig@python.org - -"""Class representing text/* type MIME documents.""" - -__all__ = ['MIMEText'] - -from bongo.external.email.encoders import encode_7or8bit -from bongo.external.email.mime.nonmultipart import MIMENonMultipart - - - -class MIMEText(MIMENonMultipart): - """Class for generating text/* type MIME documents.""" - - def __init__(self, _text, _subtype='plain', _charset='us-ascii'): - """Create a text/* type MIME document. - - _text is the string for this message object. - - _subtype is the MIME sub content type, defaulting to "plain". - - _charset is the character set parameter added to the Content-Type - header. This defaults to "us-ascii". Note that as a side-effect, the - Content-Transfer-Encoding header will also be set. - """ - MIMENonMultipart.__init__(self, 'text', _subtype, - **{'charset': _charset}) - self.set_payload(_text, _charset) diff --git a/src/libs/python/bongo/external/email/parser.py b/src/libs/python/bongo/external/email/parser.py deleted file mode 100644 index dd0a987..0000000 --- a/src/libs/python/bongo/external/email/parser.py +++ /dev/null @@ -1,91 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Barry Warsaw, Thomas Wouters, Anthony Baxter -# Contact: email-sig@python.org - -"""A parser of RFC 2822 and MIME email messages.""" - -__all__ = ['Parser', 'HeaderParser'] - -import warnings -from io import StringIO - -from bongo.external.email.feedparser import FeedParser -from bongo.external.email.message import Message - - - -class Parser: - def __init__(self, *args, **kws): - """Parser of RFC 2822 and MIME email messages. - - Creates an in-memory object tree representing the email message, which - can then be manipulated and turned over to a Generator to return the - textual representation of the message. - - The string must be formatted as a block of RFC 2822 headers and header - continuation lines, optionally preceeded by a `Unix-from' header. The - header block is terminated either by the end of the string or by a - blank line. - - _class is the class to instantiate for new message objects when they - must be created. This class must have a constructor that can take - zero arguments. Default is Message.Message. - """ - if len(args) >= 1: - if '_class' in kws: - raise TypeError("Multiple values for keyword arg '_class'") - kws['_class'] = args[0] - if len(args) == 2: - if 'strict' in kws: - raise TypeError("Multiple values for keyword arg 'strict'") - kws['strict'] = args[1] - if len(args) > 2: - raise TypeError('Too many arguments') - if '_class' in kws: - self._class = kws['_class'] - del kws['_class'] - else: - self._class = Message - if 'strict' in kws: - warnings.warn("'strict' argument is deprecated (and ignored)", - DeprecationWarning, 2) - del kws['strict'] - if kws: - raise TypeError('Unexpected keyword arguments') - - def parse(self, fp, headersonly=False): - """Create a message structure from the data in a file. - - Reads all the data from the file and returns the root of the message - structure. Optional headersonly is a flag specifying whether to stop - parsing after reading the headers or not. The default is False, - meaning it parses the entire contents of the file. - """ - feedparser = FeedParser(self._class) - if headersonly: - feedparser._set_headersonly() - while True: - data = fp.read(8192) - if not data: - break - feedparser.feed(data) - return feedparser.close() - - def parsestr(self, text, headersonly=False): - """Create a message structure from a string. - - Returns the root of the message structure. Optional headersonly is a - flag specifying whether to stop parsing after reading the headers or - not. The default is False, meaning it parses the entire contents of - the file. - """ - return self.parse(StringIO(text), headersonly=headersonly) - - - -class HeaderParser(Parser): - def parse(self, fp, headersonly=True): - return Parser.parse(self, fp, True) - - def parsestr(self, text, headersonly=True): - return Parser.parsestr(self, text, True) diff --git a/src/libs/python/bongo/external/email/payloads.py b/src/libs/python/bongo/external/email/payloads.py deleted file mode 100644 index 81dcd7f..0000000 --- a/src/libs/python/bongo/external/email/payloads.py +++ /dev/null @@ -1,154 +0,0 @@ -import tempfile -import binascii -import quopri -import uu -import base64 -from bongo.external.email import utils - -class Payload: - def __init__(self, encoding=None, value=''): - self.encoding = encoding - self.set(value) - - def set(self, buffer): - raise NotImplementedError - - def get(self, decode): - return ''.join(self.iter(decode)) - - def add(self, chunk): - raise NotImplementedError - - def strip_eol(self) : - raise NotImplementedError - - def iter(self, decode): - raise NotImplementedError - -class MemoryPayload(Payload): - def __init__(self, encoding=None, value='') : - Payload.__init__(self, encoding, value) - - def set(self, value): - self._buffer = [value] - - def add(self, chunk): - self._buffer.append(chunk) - - def strip_eol(self) : - value = ''.join(self._buffer) - if value.endswith('\r\n') : - value = value[:-2] - elif value.endswith('\r') or value.endswith('\n') : - value = value[:-1] - self._buffer = [value] - - def iter(self, decode): - if decode: - decoder = get_string_decoder(self.encoding) - else: - decoder = None - - # Just yield the payload all at once since it's all in memory already - if decoder: - yield decoder(''.join(self._buffer)) - else: - yield ''.join(self._buffer) - -class FilePayload(Payload): - - chunk_size = 8192 - - def __init__(self, encoding=None, value=''): - self._f = None - Payload.__init__(self, encoding, value) - - def __del__(self): - self._close() - - def _close(self): - if self._f: - self._f.close() - - def set(self, buf): - self._close() - self._f = tempfile.TemporaryFile() - self._f.write(buf) - - def add(self, chunk): - self._f.seek(0, 2) - self._f.write(chunk) - - def strip_eol(self) : - self._f.seek(0, 2) - toRead = self._f.tell() - if toRead > 2 : - toRead = 2 - - self._f.seek(0 - toRead, 2) - end = self._f.read(toRead) - if end == '\r\n' : - self._f.seek(-2, 2) - self._f.truncate() - elif end.endswith('\r') or end.endswith('\n') : - self._f.seek(-1, 2) - self._f.truncate() - - def iter(self, decode): - if decode: - decoder = get_file_decoder(self.encoding) - else: - decoder = None - - if decoder: - # Decode into a separate file first to ensure there are no decoding - # errors. - fout = tempfile.TemporaryFile() - self._f.seek(0, 0) - decoder(self._f, fout) - else: - fout = self._f - - # Feed out the payload in chunks - fout.seek(0, 0) - while 1: - buf = fout.read(self.chunk_size) - if buf: - yield buf - else: - break - - # If decoding occurred, close the temporary file for the decoded version - if fout != self._f: - fout.close() - -def get_string_decoder(cte): - if cte == None : - return None - - cte = cte.lower() - - if cte == 'quoted-printable': - return utils._qdecode - elif cte == 'base64': - return utils._bdecode - elif cte in ('x-uuencode', 'uuencode', 'uue', 'x-uue'): - return binascii.a2b_uu - else: - return None - -def get_file_decoder(cte): - if cte == None : - return None - - cte = cte.lower() - - if cte == 'quoted-printable': - return quopri.decode - elif cte == 'base64': - return base64.decode - elif cte in ('x-uuencode', 'uuencode', 'uue', 'x-uue'): - return uu.decode - else: - return None - diff --git a/src/libs/python/bongo/external/email/quoprimime.py b/src/libs/python/bongo/external/email/quoprimime.py deleted file mode 100644 index b3846d2..0000000 --- a/src/libs/python/bongo/external/email/quoprimime.py +++ /dev/null @@ -1,336 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Ben Gertzfield -# Contact: email-sig@python.org - -"""Quoted-printable content transfer encoding per RFCs 2045-2047. - -This module handles the content transfer encoding method defined in RFC 2045 -to encode US ASCII-like 8-bit data called `quoted-printable'. It is used to -safely encode text that is in a character set similar to the 7-bit US ASCII -character set, but that includes some 8-bit characters that are normally not -allowed in email bodies or headers. - -Quoted-printable is very space-inefficient for encoding binary files; use the -email.base64MIME module for that instead. - -This module provides an interface to encode and decode both headers and bodies -with quoted-printable encoding. - -RFC 2045 defines a method for including character set information in an -`encoded-word' in a header. This method is commonly used for 8-bit real names -in To:/From:/Cc: etc. fields, as well as Subject: lines. - -This module does not do the line wrapping or end-of-line character -conversion necessary for proper internationalized headers; it only -does dumb encoding and decoding. To deal with the various line -wrapping issues, use the email.Header module. -""" - -__all__ = [ - 'body_decode', - 'body_encode', - 'body_quopri_check', - 'body_quopri_len', - 'decode', - 'decodestring', - 'encode', - 'encodestring', - 'header_decode', - 'header_encode', - 'header_quopri_check', - 'header_quopri_len', - 'quote', - 'unquote', - ] - -import re - -from string import hexdigits -from bongo.external.email.utils import fix_eols - -CRLF = '\r\n' -NL = '\n' - -# See also Charset.py -MISC_LEN = 7 - -hqre = re.compile(r'[^-a-zA-Z0-9!*+/ ]') -bqre = re.compile(r'[^ !-<>-~\t]') - - - -# Helpers -def header_quopri_check(c): - """Return True if the character should be escaped with header quopri.""" - return bool(hqre.match(c)) - - -def body_quopri_check(c): - """Return True if the character should be escaped with body quopri.""" - return bool(bqre.match(c)) - - -def header_quopri_len(s): - """Return the length of str when it is encoded with header quopri.""" - count = 0 - for c in s: - if hqre.match(c): - count += 3 - else: - count += 1 - return count - - -def body_quopri_len(str): - """Return the length of str when it is encoded with body quopri.""" - count = 0 - for c in str: - if bqre.match(c): - count += 3 - else: - count += 1 - return count - - -def _max_append(L, s, maxlen, extra=''): - if not L: - L.append(s.lstrip()) - elif len(L[-1]) + len(s) <= maxlen: - L[-1] += extra + s - else: - L.append(s.lstrip()) - - -def unquote(s): - """Turn a string in the form =AB to the ASCII character with value 0xab""" - return chr(int(s[1:3], 16)) - - -def quote(c): - return "=%02X" % ord(c) - - - -def header_encode(header, charset="iso-8859-1", keep_eols=False, - maxlinelen=76, eol=NL): - """Encode a single header line with quoted-printable (like) encoding. - - Defined in RFC 2045, this `Q' encoding is similar to quoted-printable, but - used specifically for email header fields to allow charsets with mostly 7 - bit characters (and some 8 bit) to remain more or less readable in non-RFC - 2045 aware mail clients. - - charset names the character set to use to encode the header. It defaults - to iso-8859-1. - - The resulting string will be in the form: - - "=?charset?q?I_f=E2rt_in_your_g=E8n=E8ral_dire=E7tion?\\n - =?charset?q?Silly_=C8nglish_Kn=EEghts?=" - - with each line wrapped safely at, at most, maxlinelen characters (defaults - to 76 characters). If maxlinelen is None, the entire string is encoded in - one chunk with no splitting. - - End-of-line characters (\\r, \\n, \\r\\n) will be automatically converted - to the canonical email line separator \\r\\n unless the keep_eols - parameter is True (the default is False). - - Each line of the header will be terminated in the value of eol, which - defaults to "\\n". Set this to "\\r\\n" if you are using the result of - this function directly in email. - """ - # Return empty headers unchanged - if not header: - return header - - if not keep_eols: - header = fix_eols(header) - - # Quopri encode each line, in encoded chunks no greater than maxlinelen in - # length, after the RFC chrome is added in. - quoted = [] - if maxlinelen is None: - # An obnoxiously large number that's good enough - max_encoded = 100000 - else: - max_encoded = maxlinelen - len(charset) - MISC_LEN - 1 - - for c in header: - # Space may be represented as _ instead of =20 for readability - if c == ' ': - _max_append(quoted, '_', max_encoded) - # These characters can be included verbatim - elif not hqre.match(c): - _max_append(quoted, c, max_encoded) - # Otherwise, replace with hex value like =E2 - else: - _max_append(quoted, "=%02X" % ord(c), max_encoded) - - # Now add the RFC chrome to each encoded chunk and glue the chunks - # together. BAW: should we be able to specify the leading whitespace in - # the joiner? - joiner = eol + ' ' - return joiner.join(['=?%s?q?%s?=' % (charset, line) for line in quoted]) - - - -def encode(body, binary=False, maxlinelen=76, eol=NL): - """Encode with quoted-printable, wrapping at maxlinelen characters. - - If binary is False (the default), end-of-line characters will be converted - to the canonical email end-of-line sequence \\r\\n. Otherwise they will - be left verbatim. - - Each line of encoded text will end with eol, which defaults to "\\n". Set - this to "\\r\\n" if you will be using the result of this function directly - in an email. - - Each line will be wrapped at, at most, maxlinelen characters (defaults to - 76 characters). Long lines will have the `soft linefeed' quoted-printable - character "=" appended to them, so the decoded text will be identical to - the original text. - """ - if not body: - return body - - if not binary: - body = fix_eols(body) - - # BAW: We're accumulating the body text by string concatenation. That - # can't be very efficient, but I don't have time now to rewrite it. It - # just feels like this algorithm could be more efficient. - encoded_body = '' - lineno = -1 - # Preserve line endings here so we can check later to see an eol needs to - # be added to the output later. - lines = body.splitlines(1) - for line in lines: - # But strip off line-endings for processing this line. - if line.endswith(CRLF): - line = line[:-2] - elif line[-1] in CRLF: - line = line[:-1] - - lineno += 1 - encoded_line = '' - prev = None - linelen = len(line) - # Now we need to examine every character to see if it needs to be - # quopri encoded. BAW: again, string concatenation is inefficient. - for j in range(linelen): - c = line[j] - prev = c - if bqre.match(c): - c = quote(c) - elif j+1 == linelen: - # Check for whitespace at end of line; special case - if c not in ' \t': - encoded_line += c - prev = c - continue - # Check to see to see if the line has reached its maximum length - if len(encoded_line) + len(c) >= maxlinelen: - encoded_body += encoded_line + '=' + eol - encoded_line = '' - encoded_line += c - # Now at end of line.. - if prev and prev in ' \t': - # Special case for whitespace at end of file - if lineno + 1 == len(lines): - prev = quote(prev) - if len(encoded_line) + len(prev) > maxlinelen: - encoded_body += encoded_line + '=' + eol + prev - else: - encoded_body += encoded_line + prev - # Just normal whitespace at end of line - else: - encoded_body += encoded_line + prev + '=' + eol - encoded_line = '' - # Now look at the line we just finished and it has a line ending, we - # need to add eol to the end of the line. - if lines[lineno].endswith(CRLF) or lines[lineno][-1] in CRLF: - encoded_body += encoded_line + eol - else: - encoded_body += encoded_line - encoded_line = '' - return encoded_body - - -# For convenience and backwards compatibility w/ standard base64 module -body_encode = encode -encodestring = encode - - - -# BAW: I'm not sure if the intent was for the signature of this function to be -# the same as base64MIME.decode() or not... -def decode(encoded, eol=NL): - """Decode a quoted-printable string. - - Lines are separated with eol, which defaults to \\n. - """ - if not encoded: - return encoded - # BAW: see comment in encode() above. Again, we're building up the - # decoded string with string concatenation, which could be done much more - # efficiently. - decoded = '' - - for line in encoded.splitlines(): - line = line.rstrip() - if not line: - decoded += eol - continue - - i = 0 - n = len(line) - while i < n: - c = line[i] - if c != '=': - decoded += c - i += 1 - # Otherwise, c == "=". Are we at the end of the line? If so, add - # a soft line break. - elif i+1 == n: - i += 1 - continue - # Decode if in form =AB - elif i+2 < n and line[i+1] in hexdigits and line[i+2] in hexdigits: - decoded += unquote(line[i:i+3]) - i += 3 - # Otherwise, not in form =AB, pass literally - else: - decoded += c - i += 1 - - if i == n: - decoded += eol - # Special case if original string did not end with eol - if not encoded.endswith(eol) and decoded.endswith(eol): - decoded = decoded[:-1] - return decoded - - -# For convenience and backwards compatibility w/ standard base64 module -body_decode = decode -decodestring = decode - - - -def _unquote_match(match): - """Turn a match in the form =AB to the ASCII character with value 0xab""" - s = match.group(0) - return unquote(s) - - -# Header decoding is done a bit differently -def header_decode(s): - """Decode a string encoded with RFC 2045 MIME header `Q' encoding. - - This function does not parse a full MIME header value encoded with - quoted-printable (like =?iso-8895-1?q?Hello_World?=) -- please use - the high level email.Header class for that functionality. - """ - s = s.replace('_', ' ') - return re.sub(r'=\w{2}', _unquote_match, s) diff --git a/src/libs/python/bongo/external/email/test/__init__.py b/src/libs/python/bongo/external/email/test/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/src/libs/python/bongo/external/email/test/data/PyBanner048.gif b/src/libs/python/bongo/external/email/test/data/PyBanner048.gif deleted file mode 100644 index 1a5c87f647fbf33e5b46103119c9fd42afbe9e5d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 954 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 -Delivered-To: bbb@zzz.org -Received: by mail.zzz.org (Postfix, from userid 889) - id 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT) -MIME-Version: 1.0 -Content-Type: text/plain; charset=us-ascii -Content-Transfer-Encoding: 7bit -Message-ID: <15090.61304.110929.45684@aaa.zzz.org> -From: bbb@ddd.com (John X. Doe) -To: bbb@zzz.org -Subject: This is a test message -Date: Fri, 4 May 2001 14:05:44 -0400 - - -Hi, - -Do you like this message? - --Me diff --git a/src/libs/python/bongo/external/email/test/data/msg_02.txt b/src/libs/python/bongo/external/email/test/data/msg_02.txt deleted file mode 100644 index 43f2480..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_02.txt +++ /dev/null @@ -1,135 +0,0 @@ -MIME-version: 1.0 -From: ppp-request@zzz.org -Sender: ppp-admin@zzz.org -To: ppp@zzz.org -Subject: Ppp digest, Vol 1 #2 - 5 msgs -Date: Fri, 20 Apr 2001 20:18:00 -0400 (EDT) -X-Mailer: Mailman v2.0.4 -X-Mailman-Version: 2.0.4 -Content-Type: multipart/mixed; boundary="192.168.1.2.889.32614.987812255.500.21814" - ---192.168.1.2.889.32614.987812255.500.21814 -Content-type: text/plain; charset=us-ascii -Content-description: Masthead (Ppp digest, Vol 1 #2) - -Send Ppp mailing list submissions to - ppp@zzz.org - -To subscribe or unsubscribe via the World Wide Web, visit - http://www.zzz.org/mailman/listinfo/ppp -or, via email, send a message with subject or body 'help' to - ppp-request@zzz.org - -You can reach the person managing the list at - ppp-admin@zzz.org - -When replying, please edit your Subject line so it is more specific -than "Re: Contents of Ppp digest..." - - ---192.168.1.2.889.32614.987812255.500.21814 -Content-type: text/plain; charset=us-ascii -Content-description: Today's Topics (5 msgs) - -Today's Topics: - - 1. testing #1 (Barry A. Warsaw) - 2. testing #2 (Barry A. Warsaw) - 3. testing #3 (Barry A. Warsaw) - 4. testing #4 (Barry A. Warsaw) - 5. testing #5 (Barry A. Warsaw) - ---192.168.1.2.889.32614.987812255.500.21814 -Content-Type: multipart/digest; boundary="__--__--" - ---__--__-- - -Message: 1 -Content-Type: text/plain; charset=us-ascii -Content-Transfer-Encoding: 7bit -Date: Fri, 20 Apr 2001 20:16:13 -0400 -To: ppp@zzz.org -From: barry@digicool.com (Barry A. Warsaw) -Subject: [Ppp] testing #1 -Precedence: bulk - - -hello - - ---__--__-- - -Message: 2 -Date: Fri, 20 Apr 2001 20:16:21 -0400 -Content-Type: text/plain; charset=us-ascii -Content-Transfer-Encoding: 7bit -To: ppp@zzz.org -From: barry@digicool.com (Barry A. Warsaw) -Precedence: bulk - - -hello - - ---__--__-- - -Message: 3 -Date: Fri, 20 Apr 2001 20:16:25 -0400 -Content-Type: text/plain; charset=us-ascii -Content-Transfer-Encoding: 7bit -To: ppp@zzz.org -From: barry@digicool.com (Barry A. Warsaw) -Subject: [Ppp] testing #3 -Precedence: bulk - - -hello - - ---__--__-- - -Message: 4 -Date: Fri, 20 Apr 2001 20:16:28 -0400 -Content-Type: text/plain; charset=us-ascii -Content-Transfer-Encoding: 7bit -To: ppp@zzz.org -From: barry@digicool.com (Barry A. Warsaw) -Subject: [Ppp] testing #4 -Precedence: bulk - - -hello - - ---__--__-- - -Message: 5 -Date: Fri, 20 Apr 2001 20:16:32 -0400 -Content-Type: text/plain; charset=us-ascii -Content-Transfer-Encoding: 7bit -To: ppp@zzz.org -From: barry@digicool.com (Barry A. Warsaw) -Subject: [Ppp] testing #5 -Precedence: bulk - - -hello - - - - ---__--__---- ---192.168.1.2.889.32614.987812255.500.21814 -Content-type: text/plain; charset=us-ascii -Content-description: Digest Footer - -_______________________________________________ -Ppp mailing list -Ppp@zzz.org -http://www.zzz.org/mailman/listinfo/ppp - - ---192.168.1.2.889.32614.987812255.500.21814-- - -End of Ppp Digest - diff --git a/src/libs/python/bongo/external/email/test/data/msg_03.txt b/src/libs/python/bongo/external/email/test/data/msg_03.txt deleted file mode 100644 index c748ebf..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_03.txt +++ /dev/null @@ -1,16 +0,0 @@ -Return-Path: -Delivered-To: bbb@zzz.org -Received: by mail.zzz.org (Postfix, from userid 889) - id 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT) -Message-ID: <15090.61304.110929.45684@aaa.zzz.org> -From: bbb@ddd.com (John X. Doe) -To: bbb@zzz.org -Subject: This is a test message -Date: Fri, 4 May 2001 14:05:44 -0400 - - -Hi, - -Do you like this message? - --Me diff --git a/src/libs/python/bongo/external/email/test/data/msg_04.txt b/src/libs/python/bongo/external/email/test/data/msg_04.txt deleted file mode 100644 index 1f633c4..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_04.txt +++ /dev/null @@ -1,37 +0,0 @@ -Return-Path: -Delivered-To: barry@python.org -Received: by mail.python.org (Postfix, from userid 889) - id C2BF0D37C6; Tue, 11 Sep 2001 00:05:05 -0400 (EDT) -MIME-Version: 1.0 -Content-Type: multipart/mixed; boundary="h90VIIIKmx" -Content-Transfer-Encoding: 7bit -Message-ID: <15261.36209.358846.118674@anthem.python.org> -From: barry@python.org (Barry A. Warsaw) -To: barry@python.org -Subject: a simple multipart -Date: Tue, 11 Sep 2001 00:05:05 -0400 -X-Mailer: VM 6.95 under 21.4 (patch 4) "Artificial Intelligence" XEmacs Lucid -X-Attribution: BAW -X-Oblique-Strategy: Make a door into a window - - ---h90VIIIKmx -Content-Type: text/plain -Content-Disposition: inline; - filename="msg.txt" -Content-Transfer-Encoding: 7bit - -a simple kind of mirror -to reflect upon our own - ---h90VIIIKmx -Content-Type: text/plain -Content-Disposition: inline; - filename="msg.txt" -Content-Transfer-Encoding: 7bit - -a simple kind of mirror -to reflect upon our own - ---h90VIIIKmx-- - diff --git a/src/libs/python/bongo/external/email/test/data/msg_05.txt b/src/libs/python/bongo/external/email/test/data/msg_05.txt deleted file mode 100644 index 87d5e9c..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_05.txt +++ /dev/null @@ -1,28 +0,0 @@ -From: foo -Subject: bar -To: baz -MIME-Version: 1.0 -Content-Type: multipart/report; report-type=delivery-status; - boundary="D1690A7AC1.996856090/mail.example.com" -Message-Id: <20010803162810.0CA8AA7ACC@mail.example.com> - -This is a MIME-encapsulated message. - ---D1690A7AC1.996856090/mail.example.com -Content-Type: text/plain - -Yadda yadda yadda - ---D1690A7AC1.996856090/mail.example.com - -Yadda yadda yadda - ---D1690A7AC1.996856090/mail.example.com -Content-Type: message/rfc822 - -From: nobody@python.org - -Yadda yadda yadda - ---D1690A7AC1.996856090/mail.example.com-- - diff --git a/src/libs/python/bongo/external/email/test/data/msg_06.txt b/src/libs/python/bongo/external/email/test/data/msg_06.txt deleted file mode 100644 index 69f3a47..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_06.txt +++ /dev/null @@ -1,33 +0,0 @@ -Return-Path: -Delivered-To: barry@python.org -MIME-Version: 1.0 -Content-Type: message/rfc822 -Content-Description: forwarded message -Content-Transfer-Encoding: 7bit -Message-ID: <15265.9482.641338.555352@python.org> -From: barry@zope.com (Barry A. Warsaw) -Sender: barry@python.org -To: barry@python.org -Subject: forwarded message from Barry A. Warsaw -Date: Thu, 13 Sep 2001 17:28:42 -0400 -X-Mailer: VM 6.95 under 21.4 (patch 4) "Artificial Intelligence" XEmacs Lucid -X-Attribution: BAW -X-Oblique-Strategy: Be dirty -X-Url: http://barry.wooz.org - -MIME-Version: 1.0 -Content-Type: text/plain; charset=us-ascii -Return-Path: -Delivered-To: barry@python.org -Message-ID: <15265.9468.713530.98441@python.org> -From: barry@zope.com (Barry A. Warsaw) -Sender: barry@python.org -To: barry@python.org -Subject: testing -Date: Thu, 13 Sep 2001 17:28:28 -0400 -X-Mailer: VM 6.95 under 21.4 (patch 4) "Artificial Intelligence" XEmacs Lucid -X-Attribution: BAW -X-Oblique-Strategy: Spectrum analysis -X-Url: http://barry.wooz.org - - diff --git a/src/libs/python/bongo/external/email/test/data/msg_07.txt b/src/libs/python/bongo/external/email/test/data/msg_07.txt deleted file mode 100644 index 721f3a0..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_07.txt +++ /dev/null @@ -1,83 +0,0 @@ -MIME-Version: 1.0 -From: Barry -To: Dingus Lovers -Subject: Here is your dingus fish -Date: Fri, 20 Apr 2001 19:35:02 -0400 -Content-Type: multipart/mixed; boundary="BOUNDARY" - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" - -Hi there, - -This is the dingus fish. - ---BOUNDARY -Content-Type: image/gif; name="dingusfish.gif" -Content-Transfer-Encoding: base64 -content-disposition: attachment; filename="dingusfish.gif" - -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 - ---BOUNDARY-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_08.txt b/src/libs/python/bongo/external/email/test/data/msg_08.txt deleted file mode 100644 index b563083..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_08.txt +++ /dev/null @@ -1,24 +0,0 @@ -MIME-Version: 1.0 -From: Barry Warsaw -To: Dingus Lovers -Subject: Lyrics -Date: Fri, 20 Apr 2001 19:35:02 -0400 -Content-Type: multipart/mixed; boundary="BOUNDARY" - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" - - ---BOUNDARY -Content-Type: text/html; charset="iso-8859-1" - - ---BOUNDARY -Content-Type: text/plain; charset="iso-8859-2" - - ---BOUNDARY -Content-Type: text/plain; charset="koi8-r" - - ---BOUNDARY-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_09.txt b/src/libs/python/bongo/external/email/test/data/msg_09.txt deleted file mode 100644 index 575c4c2..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_09.txt +++ /dev/null @@ -1,24 +0,0 @@ -MIME-Version: 1.0 -From: Barry Warsaw -To: Dingus Lovers -Subject: Lyrics -Date: Fri, 20 Apr 2001 19:35:02 -0400 -Content-Type: multipart/mixed; boundary="BOUNDARY" - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" - - ---BOUNDARY -Content-Type: text/html; charset="iso-8859-1" - - ---BOUNDARY -Content-Type: text/plain - - ---BOUNDARY -Content-Type: text/plain; charset="koi8-r" - - ---BOUNDARY-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_10.txt b/src/libs/python/bongo/external/email/test/data/msg_10.txt deleted file mode 100644 index bd30d13..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_10.txt +++ /dev/null @@ -1,32 +0,0 @@ -MIME-Version: 1.0 -From: Barry Warsaw -To: Dingus Lovers -Subject: Lyrics -Date: Fri, 20 Apr 2001 19:35:02 -0400 -Content-Type: multipart/mixed; boundary="BOUNDARY" - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -Content-Transfer-Encoding: 7bit - -This is a 7bit encoded message. - ---BOUNDARY -Content-Type: text/html; charset="iso-8859-1" -Content-Transfer-Encoding: Quoted-Printable - -=A1This is a Quoted Printable encoded message! - ---BOUNDARY -Content-Type: text/plain; charset="iso-8859-1" -Content-Transfer-Encoding: Base64 - -VGhpcyBpcyBhIEJhc2U2NCBlbmNvZGVkIG1lc3NhZ2Uu - - ---BOUNDARY -Content-Type: text/plain; charset="iso-8859-1" - -This has no Content-Transfer-Encoding: header. - ---BOUNDARY-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_11.txt b/src/libs/python/bongo/external/email/test/data/msg_11.txt deleted file mode 100644 index 8f7f199..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_11.txt +++ /dev/null @@ -1,7 +0,0 @@ -Content-Type: message/rfc822 -MIME-Version: 1.0 -Subject: The enclosing message - -Subject: An enclosed message - -Here is the body of the message. diff --git a/src/libs/python/bongo/external/email/test/data/msg_12.txt b/src/libs/python/bongo/external/email/test/data/msg_12.txt deleted file mode 100644 index 4bec8d9..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_12.txt +++ /dev/null @@ -1,36 +0,0 @@ -MIME-Version: 1.0 -From: Barry Warsaw -To: Dingus Lovers -Subject: Lyrics -Date: Fri, 20 Apr 2001 19:35:02 -0400 -Content-Type: multipart/mixed; boundary="BOUNDARY" - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" - - ---BOUNDARY -Content-Type: text/html; charset="iso-8859-1" - - ---BOUNDARY -Content-Type: multipart/mixed; boundary="ANOTHER" - ---ANOTHER -Content-Type: text/plain; charset="iso-8859-2" - - ---ANOTHER -Content-Type: text/plain; charset="iso-8859-3" - ---ANOTHER-- - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" - - ---BOUNDARY -Content-Type: text/plain; charset="koi8-r" - - ---BOUNDARY-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_12a.txt b/src/libs/python/bongo/external/email/test/data/msg_12a.txt deleted file mode 100644 index e94224e..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_12a.txt +++ /dev/null @@ -1,38 +0,0 @@ -MIME-Version: 1.0 -From: Barry Warsaw -To: Dingus Lovers -Subject: Lyrics -Date: Fri, 20 Apr 2001 19:35:02 -0400 -Content-Type: multipart/mixed; boundary="BOUNDARY" - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" - - ---BOUNDARY -Content-Type: text/html; charset="iso-8859-1" - - ---BOUNDARY -Content-Type: multipart/mixed; boundary="ANOTHER" - ---ANOTHER -Content-Type: text/plain; charset="iso-8859-2" - - ---ANOTHER -Content-Type: text/plain; charset="iso-8859-3" - - ---ANOTHER-- - - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" - - ---BOUNDARY -Content-Type: text/plain; charset="koi8-r" - - ---BOUNDARY-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_13.txt b/src/libs/python/bongo/external/email/test/data/msg_13.txt deleted file mode 100644 index 8e6d52d..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_13.txt +++ /dev/null @@ -1,94 +0,0 @@ -MIME-Version: 1.0 -From: Barry -To: Dingus Lovers -Subject: Here is your dingus fish -Date: Fri, 20 Apr 2001 19:35:02 -0400 -Content-Type: multipart/mixed; boundary="OUTER" - ---OUTER -Content-Type: text/plain; charset="us-ascii" - -A text/plain part - ---OUTER -Content-Type: multipart/mixed; boundary=BOUNDARY - - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" - -Hi there, - -This is the dingus fish. - ---BOUNDARY -Content-Type: image/gif; name="dingusfish.gif" -Content-Transfer-Encoding: base64 -content-disposition: attachment; filename="dingusfish.gif" - -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 - ---BOUNDARY-- - ---OUTER-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_14.txt b/src/libs/python/bongo/external/email/test/data/msg_14.txt deleted file mode 100644 index 5d98d2f..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_14.txt +++ /dev/null @@ -1,23 +0,0 @@ -Return-Path: -Delivered-To: bbb@zzz.org -Received: by mail.zzz.org (Postfix, from userid 889) - id 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT) -MIME-Version: 1.0 -Content-Type: text; charset=us-ascii -Content-Transfer-Encoding: 7bit -Message-ID: <15090.61304.110929.45684@aaa.zzz.org> -From: bbb@ddd.com (John X. Doe) -To: bbb@zzz.org -Subject: This is a test message -Date: Fri, 4 May 2001 14:05:44 -0400 - - -Hi, - -I'm sorry but I'm using a drainbread ISP, which although big and -wealthy can't seem to generate standard compliant email. :( - -This message has a Content-Type: header with no subtype. I hope you -can still read it. - --Me diff --git a/src/libs/python/bongo/external/email/test/data/msg_15.txt b/src/libs/python/bongo/external/email/test/data/msg_15.txt deleted file mode 100644 index 33b8487..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_15.txt +++ /dev/null @@ -1,52 +0,0 @@ -Return-Path: -Received: from fepD.post.tele.dk (195.41.46.149) by mail.groupcare.dk (LSMTP for Windows NT v1.1b) with SMTP id <0.0014F8A2@mail.groupcare.dk>; Mon, 30 Apr 2001 12:17:50 +0200 -User-Agent: Microsoft-Outlook-Express-Macintosh-Edition/5.02.2106 -Subject: XX -From: xx@xx.dk -To: XX -Message-ID: -Mime-version: 1.0 -Content-type: multipart/mixed; - boundary="MS_Mac_OE_3071477847_720252_MIME_Part" - -> Denne meddelelse er i MIME-format. Da dit postl¾sningsprogram ikke forstŒr dette format, kan del af eller hele meddelelsen v¾re ul¾selig. - ---MS_Mac_OE_3071477847_720252_MIME_Part -Content-type: multipart/alternative; - boundary="MS_Mac_OE_3071477847_720252_MIME_Part" - - ---MS_Mac_OE_3071477847_720252_MIME_Part -Content-type: text/plain; charset="ISO-8859-1" -Content-transfer-encoding: quoted-printable - -Some removed test. - ---MS_Mac_OE_3071477847_720252_MIME_Part -Content-type: text/html; charset="ISO-8859-1" -Content-transfer-encoding: quoted-printable - - - -Some removed HTML - - -Some removed text. - - - - ---MS_Mac_OE_3071477847_720252_MIME_Part-- - - ---MS_Mac_OE_3071477847_720252_MIME_Part -Content-type: image/gif; name="xx.gif"; - x-mac-creator="6F676C65"; - x-mac-type="47494666" -Content-disposition: attachment -Content-transfer-encoding: base64 - -Some removed base64 encoded chars. - ---MS_Mac_OE_3071477847_720252_MIME_Part-- - diff --git a/src/libs/python/bongo/external/email/test/data/msg_16.txt b/src/libs/python/bongo/external/email/test/data/msg_16.txt deleted file mode 100644 index 56167e9..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_16.txt +++ /dev/null @@ -1,123 +0,0 @@ -Return-Path: <> -Delivered-To: scr-admin@socal-raves.org -Received: from cougar.noc.ucla.edu (cougar.noc.ucla.edu [169.232.10.18]) - by babylon.socal-raves.org (Postfix) with ESMTP id CCC2C51B84 - for ; Sun, 23 Sep 2001 20:13:54 -0700 (PDT) -Received: from sims-ms-daemon by cougar.noc.ucla.edu - (Sun Internet Mail Server sims.3.5.2000.03.23.18.03.p10) - id <0GK500B01D0B8Y@cougar.noc.ucla.edu> for scr-admin@socal-raves.org; Sun, - 23 Sep 2001 20:14:35 -0700 (PDT) -Received: from cougar.noc.ucla.edu - (Sun Internet Mail Server sims.3.5.2000.03.23.18.03.p10) - id <0GK500B01D0B8X@cougar.noc.ucla.edu>; Sun, 23 Sep 2001 20:14:35 -0700 (PDT) -Date: Sun, 23 Sep 2001 20:14:35 -0700 (PDT) -From: Internet Mail Delivery -Subject: Delivery Notification: Delivery has failed -To: scr-admin@socal-raves.org -Message-id: <0GK500B04D0B8X@cougar.noc.ucla.edu> -MIME-version: 1.0 -Sender: scr-owner@socal-raves.org -Errors-To: scr-owner@socal-raves.org -X-BeenThere: scr@socal-raves.org -X-Mailman-Version: 2.1a3 -Precedence: bulk -List-Help: -List-Post: -List-Subscribe: , - -List-Id: SoCal-Raves -List-Unsubscribe: , - -List-Archive: -Content-Type: multipart/report; boundary="Boundary_(ID_PGS2F2a+z+/jL7hupKgRhA)" - - ---Boundary_(ID_PGS2F2a+z+/jL7hupKgRhA) -Content-type: text/plain; charset=ISO-8859-1 - -This report relates to a message you sent with the following header fields: - - Message-id: <002001c144a6$8752e060$56104586@oxy.edu> - Date: Sun, 23 Sep 2001 20:10:55 -0700 - From: "Ian T. Henry" - To: SoCal Raves - Subject: [scr] yeah for Ians!! - -Your message cannot be delivered to the following recipients: - - Recipient address: jangel1@cougar.noc.ucla.edu - Reason: recipient reached disk quota - - ---Boundary_(ID_PGS2F2a+z+/jL7hupKgRhA) -Content-type: message/DELIVERY-STATUS - -Original-envelope-id: 0GK500B4HD0888@cougar.noc.ucla.edu -Reporting-MTA: dns; cougar.noc.ucla.edu - -Action: failed -Status: 5.0.0 (recipient reached disk quota) -Original-recipient: rfc822;jangel1@cougar.noc.ucla.edu -Final-recipient: rfc822;jangel1@cougar.noc.ucla.edu - ---Boundary_(ID_PGS2F2a+z+/jL7hupKgRhA) -Content-type: MESSAGE/RFC822 - -Return-path: scr-admin@socal-raves.org -Received: from sims-ms-daemon by cougar.noc.ucla.edu - (Sun Internet Mail Server sims.3.5.2000.03.23.18.03.p10) - id <0GK500B01D0B8X@cougar.noc.ucla.edu>; Sun, 23 Sep 2001 20:14:35 -0700 (PDT) -Received: from panther.noc.ucla.edu by cougar.noc.ucla.edu - (Sun Internet Mail Server sims.3.5.2000.03.23.18.03.p10) - with ESMTP id <0GK500B4GD0888@cougar.noc.ucla.edu> for jangel1@sims-ms-daemon; - Sun, 23 Sep 2001 20:14:33 -0700 (PDT) -Received: from babylon.socal-raves.org - (ip-209-85-222-117.dreamhost.com [209.85.222.117]) - by panther.noc.ucla.edu (8.9.1a/8.9.1) with ESMTP id UAA09793 for - ; Sun, 23 Sep 2001 20:14:32 -0700 (PDT) -Received: from babylon (localhost [127.0.0.1]) by babylon.socal-raves.org - (Postfix) with ESMTP id D3B2951B70; Sun, 23 Sep 2001 20:13:47 -0700 (PDT) -Received: by babylon.socal-raves.org (Postfix, from userid 60001) - id A611F51B82; Sun, 23 Sep 2001 20:13:46 -0700 (PDT) -Received: from tiger.cc.oxy.edu (tiger.cc.oxy.edu [134.69.3.112]) - by babylon.socal-raves.org (Postfix) with ESMTP id ADA7351B70 for - ; Sun, 23 Sep 2001 20:13:44 -0700 (PDT) -Received: from ent (n16h86.dhcp.oxy.edu [134.69.16.86]) - by tiger.cc.oxy.edu (8.8.8/8.8.8) with SMTP id UAA08100 for - ; Sun, 23 Sep 2001 20:14:24 -0700 (PDT) -Date: Sun, 23 Sep 2001 20:10:55 -0700 -From: "Ian T. Henry" -Subject: [scr] yeah for Ians!! -Sender: scr-admin@socal-raves.org -To: SoCal Raves -Errors-to: scr-admin@socal-raves.org -Message-id: <002001c144a6$8752e060$56104586@oxy.edu> -MIME-version: 1.0 -X-Mailer: Microsoft Outlook Express 5.50.4522.1200 -Content-type: text/plain; charset=us-ascii -Precedence: bulk -Delivered-to: scr-post@babylon.socal-raves.org -Delivered-to: scr@socal-raves.org -X-Converted-To-Plain-Text: from multipart/alternative by demime 0.98e -X-Converted-To-Plain-Text: Alternative section used was text/plain -X-BeenThere: scr@socal-raves.org -X-Mailman-Version: 2.1a3 -List-Help: -List-Post: -List-Subscribe: , - -List-Id: SoCal-Raves -List-Unsubscribe: , - -List-Archive: - -I always love to find more Ian's that are over 3 years old!! - -Ian -_______________________________________________ -For event info, list questions, or to unsubscribe, see http://www.socal-raves.org/ - - - ---Boundary_(ID_PGS2F2a+z+/jL7hupKgRhA)-- - diff --git a/src/libs/python/bongo/external/email/test/data/msg_17.txt b/src/libs/python/bongo/external/email/test/data/msg_17.txt deleted file mode 100644 index 8d86e41..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_17.txt +++ /dev/null @@ -1,12 +0,0 @@ -MIME-Version: 1.0 -From: Barry -To: Dingus Lovers -Subject: Here is your dingus fish -Date: Fri, 20 Apr 2001 19:35:02 -0400 -Content-Type: multipart/mixed; boundary="BOUNDARY" - -Hi there, - -This is the dingus fish. - -[Non-text (image/gif) part of message omitted, filename dingusfish.gif] diff --git a/src/libs/python/bongo/external/email/test/data/msg_19.txt b/src/libs/python/bongo/external/email/test/data/msg_19.txt deleted file mode 100644 index 49bf7fc..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_19.txt +++ /dev/null @@ -1,43 +0,0 @@ -Send Ppp mailing list submissions to - ppp@zzz.org - -To subscribe or unsubscribe via the World Wide Web, visit - http://www.zzz.org/mailman/listinfo/ppp -or, via email, send a message with subject or body 'help' to - ppp-request@zzz.org - -You can reach the person managing the list at - ppp-admin@zzz.org - -When replying, please edit your Subject line so it is more specific -than "Re: Contents of Ppp digest..." - -Today's Topics: - - 1. testing #1 (Barry A. Warsaw) - 2. testing #2 (Barry A. Warsaw) - 3. testing #3 (Barry A. Warsaw) - 4. testing #4 (Barry A. Warsaw) - 5. testing #5 (Barry A. Warsaw) - -hello - - -hello - - -hello - - -hello - - -hello - - - -_______________________________________________ -Ppp mailing list -Ppp@zzz.org -http://www.zzz.org/mailman/listinfo/ppp - diff --git a/src/libs/python/bongo/external/email/test/data/msg_20.txt b/src/libs/python/bongo/external/email/test/data/msg_20.txt deleted file mode 100644 index 1a6a887..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_20.txt +++ /dev/null @@ -1,22 +0,0 @@ -Return-Path: -Delivered-To: bbb@zzz.org -Received: by mail.zzz.org (Postfix, from userid 889) - id 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT) -MIME-Version: 1.0 -Content-Type: text/plain; charset=us-ascii -Content-Transfer-Encoding: 7bit -Message-ID: <15090.61304.110929.45684@aaa.zzz.org> -From: bbb@ddd.com (John X. Doe) -To: bbb@zzz.org -Cc: ccc@zzz.org -CC: ddd@zzz.org -cc: eee@zzz.org -Subject: This is a test message -Date: Fri, 4 May 2001 14:05:44 -0400 - - -Hi, - -Do you like this message? - --Me diff --git a/src/libs/python/bongo/external/email/test/data/msg_21.txt b/src/libs/python/bongo/external/email/test/data/msg_21.txt deleted file mode 100644 index 23590b2..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_21.txt +++ /dev/null @@ -1,20 +0,0 @@ -From: aperson@dom.ain -To: bperson@dom.ain -Subject: Test -Content-Type: multipart/mixed; boundary="BOUNDARY" - -MIME message ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -One ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -Two ---BOUNDARY-- -End of MIME message diff --git a/src/libs/python/bongo/external/email/test/data/msg_22.txt b/src/libs/python/bongo/external/email/test/data/msg_22.txt deleted file mode 100644 index af9de5f..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_22.txt +++ /dev/null @@ -1,46 +0,0 @@ -Mime-Version: 1.0 -Message-Id: -Date: Tue, 16 Oct 2001 13:59:25 +0300 -To: a@example.com -From: b@example.com -Content-Type: multipart/mixed; boundary="============_-1208892523==_============" - ---============_-1208892523==_============ -Content-Type: text/plain; charset="us-ascii" ; format="flowed" - -Text text text. ---============_-1208892523==_============ -Content-Id: -Content-Type: image/jpeg; name="wibble.JPG" - ; x-mac-type="4A504547" - ; x-mac-creator="474B4F4E" -Content-Disposition: attachment; filename="wibble.JPG" -Content-Transfer-Encoding: base64 - -/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB -AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAALCAXABIEBAREA -g6bCjjw/pIZSjO6FWFpldjySOmCNrO7DBZibUXhTwtCixw+GtAijVdqxxaPp0aKvmGXa -qrbBQvms0mAMeYS/3iTV1dG0hHaRNK01XblnWxtVdjkHLMIgTyqnk9VB7CrP2KzIINpa -4O7I+zxYO9WV8jZg71Zlb+8rMDkEirAVQFAUAKAFAAAUAYAUDgADgY6DjpRtXj5RxjHA -4wQRj0wQCMdCAewpaKKK/9k= ---============_-1208892523==_============ -Content-Id: -Content-Type: image/jpeg; name="wibble2.JPG" - ; x-mac-type="4A504547" - ; x-mac-creator="474B4F4E" -Content-Disposition: attachment; filename="wibble2.JPG" -Content-Transfer-Encoding: base64 - -/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEB -AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQH/wAALCAXABJ0BAREA -/8QAHwAAAQUBAQEBAQEAAAAAAAAAAAECAwQFBgcICQoL/8QAtRAAAgEDAwIEAwUFBAQA -W6NFJJBEkU10kKGTcWMDwxuU+0JHvk8qAtOpNwqSR0n8c3BlDyXHlqsUltHEiTvdXLxR -7vMiGDNJAJWkAMk8ZkCFp5G2oo5W++INrbQtNfTQxJAuXlupz9oS4d5Y1W+E2XlWZJJE -Y7LWYQxTLE1zuMbfBPxw8X2fibVdIbSbI6nLZxX635t9TjtYreWR7WGKJTLJFFKSlozO -0ShxIXM43uC3/9k= ---============_-1208892523==_============ -Content-Type: text/plain; charset="us-ascii" ; format="flowed" - -Text text text. ---============_-1208892523==_============-- - diff --git a/src/libs/python/bongo/external/email/test/data/msg_23.txt b/src/libs/python/bongo/external/email/test/data/msg_23.txt deleted file mode 100644 index bb2e8ec..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_23.txt +++ /dev/null @@ -1,8 +0,0 @@ -From: aperson@dom.ain -Content-Type: multipart/mixed; boundary="BOUNDARY" - ---BOUNDARY -Content-Type: text/plain - -A message part ---BOUNDARY-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_24.txt b/src/libs/python/bongo/external/email/test/data/msg_24.txt deleted file mode 100644 index 4e52339..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_24.txt +++ /dev/null @@ -1,10 +0,0 @@ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - ---BOUNDARY - - ---BOUNDARY-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_25.txt b/src/libs/python/bongo/external/email/test/data/msg_25.txt deleted file mode 100644 index 9e35275..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_25.txt +++ /dev/null @@ -1,117 +0,0 @@ -From MAILER-DAEMON Fri Apr 06 16:46:09 2001 -Received: from [204.245.199.98] (helo=zinfandel.lacita.com) - by www.linux.org.uk with esmtp (Exim 3.13 #1) - id 14lYR6-0008Iv-00 - for linuxuser-admin@www.linux.org.uk; Fri, 06 Apr 2001 16:46:09 +0100 -Received: from localhost (localhost) by zinfandel.lacita.com (8.7.3/8.6.10-MT4.00) with internal id JAB03225; Fri, 6 Apr 2001 09:23:06 -0800 (GMT-0800) -Date: Fri, 6 Apr 2001 09:23:06 -0800 (GMT-0800) -From: Mail Delivery Subsystem -Subject: Returned mail: Too many hops 19 (17 max): from via [199.164.235.226], to -Message-Id: <200104061723.JAB03225@zinfandel.lacita.com> -To: -To: postmaster@zinfandel.lacita.com -MIME-Version: 1.0 -Content-Type: multipart/report; report-type=delivery-status; - bo -Auto-Submitted: auto-generated (failure) - -This is a MIME-encapsulated message - ---JAB03225.986577786/zinfandel.lacita.com - -The original message was received at Fri, 6 Apr 2001 09:23:03 -0800 (GMT-0800) -from [199.164.235.226] - - ----- The following addresses have delivery notifications ----- - (unrecoverable error) - - ----- Transcript of session follows ----- -554 Too many hops 19 (17 max): from via [199.164.235.226], to - ---JAB03225.986577786/zinfandel.lacita.com -Content-Type: message/delivery-status - -Reporting-MTA: dns; zinfandel.lacita.com -Received-From-MTA: dns; [199.164.235.226] -Arrival-Date: Fri, 6 Apr 2001 09:23:03 -0800 (GMT-0800) - -Final-Recipient: rfc822; scoffman@wellpartner.com -Action: failed -Status: 5.4.6 -Last-Attempt-Date: Fri, 6 Apr 2001 09:23:06 -0800 (GMT-0800) - ---JAB03225.986577786/zinfandel.lacita.com -Content-Type: text/rfc822-headers - -Return-Path: linuxuser-admin@www.linux.org.uk -Received: from ns1.wellpartner.net ([199.164.235.226]) by zinfandel.lacita.com (8.7.3/8.6.10-MT4.00) with ESMTP id JAA03225 for ; Fri, 6 Apr 2001 09:23:03 -0800 (GMT-0800) -Received: from zinfandel.lacita.com ([204.245.199.98]) - by - fo -Received: from ns1.wellpartner.net ([199.164.235.226]) by zinfandel.lacita.com (8.7.3/8.6.10-MT4.00) with ESMTP id JAA03221 for ; Fri, 6 Apr 2001 09:22:18 -0800 (GMT-0800) -Received: from zinfandel.lacita.com ([204.245.199.98]) - by - fo -Received: from ns1.wellpartner.net ([199.164.235.226]) by zinfandel.lacita.com (8.7.3/8.6.10-MT4.00) with ESMTP id JAA03217 for ; Fri, 6 Apr 2001 09:21:37 -0800 (GMT-0800) -Received: from zinfandel.lacita.com ([204.245.199.98]) - by - fo -Received: from ns1.wellpartner.net ([199.164.235.226]) by zinfandel.lacita.com (8.7.3/8.6.10-MT4.00) with ESMTP id JAA03213 for ; Fri, 6 Apr 2001 09:20:56 -0800 (GMT-0800) -Received: from zinfandel.lacita.com ([204.245.199.98]) - by - fo -Received: from ns1.wellpartner.net ([199.164.235.226]) by zinfandel.lacita.com (8.7.3/8.6.10-MT4.00) with ESMTP id JAA03209 for ; Fri, 6 Apr 2001 09:20:15 -0800 (GMT-0800) -Received: from zinfandel.lacita.com ([204.245.199.98]) - by - fo -Received: from ns1.wellpartner.net ([199.164.235.226]) by zinfandel.lacita.com (8.7.3/8.6.10-MT4.00) with ESMTP id JAA03205 for ; Fri, 6 Apr 2001 09:19:33 -0800 (GMT-0800) -Received: from zinfandel.lacita.com ([204.245.199.98]) - by - fo -Received: from ns1.wellpartner.net ([199.164.235.226]) by zinfandel.lacita.com (8.7.3/8.6.10-MT4.00) with ESMTP id JAA03201 for ; Fri, 6 Apr 2001 09:18:52 -0800 (GMT-0800) -Received: from zinfandel.lacita.com ([204.245.199.98]) - by - fo -Received: from ns1.wellpartner.net ([199.164.235.226]) by zinfandel.lacita.com (8.7.3/8.6.10-MT4.00) with ESMTP id JAA03197 for ; Fri, 6 Apr 2001 09:17:54 -0800 (GMT-0800) -Received: from www.linux.org.uk (parcelfarce.linux.theplanet.co.uk [195.92.249.252]) - by - fo -Received: from localhost.localdomain - ([ - by - id -Received: from [212.1.130.11] (helo=s1.uklinux.net ident=root) - by - id - fo -Received: from server (ppp-2-22.cvx4.telinco.net [212.1.149.22]) - by - fo -From: Daniel James -Organization: LinuxUser -To: linuxuser@www.linux.org.uk -X-Mailer: KMail [version 1.1.99] -Content-Type: text/plain; - c -MIME-Version: 1.0 -Message-Id: <01040616033903.00962@server> -Content-Transfer-Encoding: 8bit -Subject: [LinuxUser] bulletin no. 45 -Sender: linuxuser-admin@www.linux.org.uk -Errors-To: linuxuser-admin@www.linux.org.uk -X-BeenThere: linuxuser@www.linux.org.uk -X-Mailman-Version: 2.0.3 -Precedence: bulk -List-Help: -List-Post: -List-Subscribe: , - -List-Unsubscribe: , - -Date: Fri, 6 Apr 2001 16:03:39 +0100 - ---JAB03225.986577786/zinfandel.lacita.com-- - - diff --git a/src/libs/python/bongo/external/email/test/data/msg_26.txt b/src/libs/python/bongo/external/email/test/data/msg_26.txt deleted file mode 100644 index 6c71bce..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_26.txt +++ /dev/null @@ -1,45 +0,0 @@ -Received: from xcar [192.168.0.2] by jeeves.wooster.local - (SMTPD32-7.07 EVAL) id AFF92F0214; Sun, 12 May 2002 08:55:37 +0100 -Date: Sun, 12 May 2002 08:56:15 +0100 -From: Father Time -To: timbo@jeeves.wooster.local -Subject: IMAP file test -Message-ID: <6df65d354b.father.time@rpc.wooster.local> -X-Organization: Home -User-Agent: Messenger-Pro/2.50a (MsgServe/1.50) (RISC-OS/4.02) POPstar/2.03 -MIME-Version: 1.0 -Content-Type: multipart/mixed; boundary="1618492860--2051301190--113853680" -Status: R -X-UIDL: 319998302 - -This message is in MIME format which your mailer apparently does not support. -You either require a newer version of your software which supports MIME, or -a separate MIME decoding utility. Alternatively, ask the sender of this -message to resend it in a different format. - ---1618492860--2051301190--113853680 -Content-Type: text/plain; charset=us-ascii - -Simple email with attachment. - - ---1618492860--2051301190--113853680 -Content-Type: application/riscos; name="clock.bmp,69c"; type=BMP; load=&fff69c4b; exec=&355dd4d1; access=&03 -Content-Disposition: attachment; filename="clock.bmp" -Content-Transfer-Encoding: base64 - -Qk12AgAAAAAAAHYAAAAoAAAAIAAAACAAAAABAAQAAAAAAAAAAADXDQAA1w0AAAAAAAAA -AAAAAAAAAAAAiAAAiAAAAIiIAIgAAACIAIgAiIgAALu7uwCIiIgAERHdACLuIgAz//8A -zAAAAN0R3QDu7iIA////AAAAAAAAAAAAAAAAAAAAAAAAAAi3AAAAAAAAADeAAAAAAAAA -C3ADMzMzMANwAAAAAAAAAAAHMAAAAANwAAAAAAAAAACAMAd3zPfwAwgAAAAAAAAIAwd/ -f8x/f3AwgAAAAAAAgDB0x/f3//zPAwgAAAAAAAcHfM9////8z/AwAAAAAAiwd/f3//// -////A4AAAAAAcEx/f///////zAMAAAAAiwfM9////3///8zwOAAAAAcHf3////B///// -8DAAAAALB/f3///wd3d3//AwAAAABwTPf//wCQAAD/zAMAAAAAsEx/f///B////8wDAA -AAAHB39////wf/////AwAAAACwf39///8H/////wMAAAAIcHfM9///B////M8DgAAAAA -sHTH///wf///xAMAAAAACHB3f3//8H////cDgAAAAAALB3zH//D//M9wMAAAAAAAgLB0 -z39///xHAwgAAAAAAAgLB3d3RHd3cDCAAAAAAAAAgLAHd0R3cAMIAAAAAAAAgAgLcAAA -AAMwgAgAAAAACDAAAAu7t7cwAAgDgAAAAABzcIAAAAAAAAgDMwAAAAAAN7uwgAAAAAgH -MzMAAAAACH97tzAAAAALu3c3gAAAAAAL+7tzDABAu7f7cAAAAAAACA+3MA7EQAv/sIAA -AAAAAAAIAAAAAAAAAIAAAAAA - ---1618492860--2051301190--113853680-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_27.txt b/src/libs/python/bongo/external/email/test/data/msg_27.txt deleted file mode 100644 index d019176..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_27.txt +++ /dev/null @@ -1,15 +0,0 @@ -Return-Path: -Received: by mail.dom.ain (Postfix, from userid 889) - id B9D0AD35DB; Tue, 4 Jun 2002 21:46:59 -0400 (EDT) -Message-ID: <15613.28051.707126.569693@dom.ain> -Date: Tue, 4 Jun 2002 21:46:59 -0400 -MIME-Version: 1.0 -Content-Type: text/plain; charset=us-ascii -Content-Transfer-Encoding: 7bit -Subject: bug demonstration - 12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789 - more text -From: aperson@dom.ain (Anne P. Erson) -To: bperson@dom.ain (Barney P. Erson) - -test diff --git a/src/libs/python/bongo/external/email/test/data/msg_28.txt b/src/libs/python/bongo/external/email/test/data/msg_28.txt deleted file mode 100644 index 1e4824c..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_28.txt +++ /dev/null @@ -1,25 +0,0 @@ -From: aperson@dom.ain -MIME-Version: 1.0 -Content-Type: multipart/digest; boundary=BOUNDARY - ---BOUNDARY -Content-Type: message/rfc822 - -Content-Type: text/plain; charset=us-ascii -To: aa@bb.org -From: cc@dd.org -Subject: ee - -message 1 - ---BOUNDARY -Content-Type: message/rfc822 - -Content-Type: text/plain; charset=us-ascii -To: aa@bb.org -From: cc@dd.org -Subject: ee - -message 2 - ---BOUNDARY-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_29.txt b/src/libs/python/bongo/external/email/test/data/msg_29.txt deleted file mode 100644 index 1fab561..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_29.txt +++ /dev/null @@ -1,22 +0,0 @@ -Return-Path: -Delivered-To: bbb@zzz.org -Received: by mail.zzz.org (Postfix, from userid 889) - id 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT) -MIME-Version: 1.0 -Content-Type: text/plain; charset=us-ascii; - title*0*="us-ascii'en'This%20is%20even%20more%20"; - title*1*="%2A%2A%2Afun%2A%2A%2A%20"; - title*2="isn't it!" -Content-Transfer-Encoding: 7bit -Message-ID: <15090.61304.110929.45684@aaa.zzz.org> -From: bbb@ddd.com (John X. Doe) -To: bbb@zzz.org -Subject: This is a test message -Date: Fri, 4 May 2001 14:05:44 -0400 - - -Hi, - -Do you like this message? - --Me diff --git a/src/libs/python/bongo/external/email/test/data/msg_30.txt b/src/libs/python/bongo/external/email/test/data/msg_30.txt deleted file mode 100644 index 4334bb6..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_30.txt +++ /dev/null @@ -1,23 +0,0 @@ -From: aperson@dom.ain -MIME-Version: 1.0 -Content-Type: multipart/digest; boundary=BOUNDARY - ---BOUNDARY - -Content-Type: text/plain; charset=us-ascii -To: aa@bb.org -From: cc@dd.org -Subject: ee - -message 1 - ---BOUNDARY - -Content-Type: text/plain; charset=us-ascii -To: aa@bb.org -From: cc@dd.org -Subject: ee - -message 2 - ---BOUNDARY-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_31.txt b/src/libs/python/bongo/external/email/test/data/msg_31.txt deleted file mode 100644 index 1e58e56..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_31.txt +++ /dev/null @@ -1,15 +0,0 @@ -From: aperson@dom.ain -MIME-Version: 1.0 -Content-Type: multipart/mixed; boundary=BOUNDARY_ - ---BOUNDARY -Content-Type: text/plain - -message 1 - ---BOUNDARY -Content-Type: text/plain - -message 2 - ---BOUNDARY-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_32.txt b/src/libs/python/bongo/external/email/test/data/msg_32.txt deleted file mode 100644 index 07ec5af..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_32.txt +++ /dev/null @@ -1,14 +0,0 @@ -Delivered-To: freebsd-isp@freebsd.org -Date: Tue, 26 Sep 2000 12:23:03 -0500 -From: Anne Person -To: Barney Dude -Subject: Re: Limiting Perl CPU Utilization... -Mime-Version: 1.0 -Content-Type: text/plain; charset*=ansi-x3.4-1968''us-ascii -Content-Disposition: inline -User-Agent: Mutt/1.3.8i -Sender: owner-freebsd-isp@FreeBSD.ORG -Precedence: bulk -X-Loop: FreeBSD.org - -Some message. diff --git a/src/libs/python/bongo/external/email/test/data/msg_33.txt b/src/libs/python/bongo/external/email/test/data/msg_33.txt deleted file mode 100644 index 042787a..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_33.txt +++ /dev/null @@ -1,29 +0,0 @@ -Delivered-To: freebsd-isp@freebsd.org -Date: Wed, 27 Sep 2000 11:11:09 -0500 -From: Anne Person -To: Barney Dude -Subject: Re: Limiting Perl CPU Utilization... -Mime-Version: 1.0 -Content-Type: multipart/signed; micalg*=ansi-x3.4-1968''pgp-md5; - protocol*=ansi-x3.4-1968''application%2Fpgp-signature; - boundary*="ansi-x3.4-1968''EeQfGwPcQSOJBaQU" -Content-Disposition: inline -Sender: owner-freebsd-isp@FreeBSD.ORG -Precedence: bulk -X-Loop: FreeBSD.org - - ---EeQfGwPcQSOJBaQU -Content-Type: text/plain; charset*=ansi-x3.4-1968''us-ascii -Content-Disposition: inline -Content-Transfer-Encoding: quoted-printable - -part 1 - ---EeQfGwPcQSOJBaQU -Content-Type: text/plain -Content-Disposition: inline - -part 2 - ---EeQfGwPcQSOJBaQU-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_34.txt b/src/libs/python/bongo/external/email/test/data/msg_34.txt deleted file mode 100644 index 055dfea..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_34.txt +++ /dev/null @@ -1,19 +0,0 @@ -From: aperson@dom.ain -To: bperson@dom.ain -Content-Type: multipart/digest; boundary=XYZ - ---XYZ -Content-Type: text/plain - - -This is a text plain part that is counter to recommended practice in -RFC 2046, $5.1.5, but is not illegal - ---XYZ - -From: cperson@dom.ain -To: dperson@dom.ain - -A submessage - ---XYZ-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_35.txt b/src/libs/python/bongo/external/email/test/data/msg_35.txt deleted file mode 100644 index be7d5a2..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_35.txt +++ /dev/null @@ -1,4 +0,0 @@ -From: aperson@dom.ain -To: bperson@dom.ain -Subject: here's something interesting -counter to RFC 2822, there's no separating newline here diff --git a/src/libs/python/bongo/external/email/test/data/msg_36.txt b/src/libs/python/bongo/external/email/test/data/msg_36.txt deleted file mode 100644 index 5632c30..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_36.txt +++ /dev/null @@ -1,40 +0,0 @@ -Mime-Version: 1.0 -Content-Type: Multipart/Mixed; Boundary="NextPart" -To: IETF-Announce:; -From: Internet-Drafts@ietf.org -Subject: I-D ACTION:draft-ietf-mboned-mix-00.txt -Date: Tue, 22 Dec 1998 16:55:06 -0500 - ---NextPart - -Blah blah blah - ---NextPart -Content-Type: Multipart/Alternative; Boundary="OtherAccess" - ---OtherAccess -Content-Type: Message/External-body; - access-type="mail-server"; - server="mailserv@ietf.org" - -Content-Type: text/plain -Content-ID: <19981222151406.I-D@ietf.org> - -ENCODING mime -FILE /internet-drafts/draft-ietf-mboned-mix-00.txt - ---OtherAccess -Content-Type: Message/External-body; - name="draft-ietf-mboned-mix-00.txt"; - site="ftp.ietf.org"; - access-type="anon-ftp"; - directory="internet-drafts" - -Content-Type: text/plain -Content-ID: <19981222151406.I-D@ietf.org> - - ---OtherAccess-- - ---NextPart-- - diff --git a/src/libs/python/bongo/external/email/test/data/msg_37.txt b/src/libs/python/bongo/external/email/test/data/msg_37.txt deleted file mode 100644 index 038d34a..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_37.txt +++ /dev/null @@ -1,22 +0,0 @@ -Content-Type: multipart/mixed; boundary=ABCDE - ---ABCDE -Content-Type: text/x-one - -Blah - ---ABCDE ---ABCDE -Content-Type: text/x-two - -Blah - ---ABCDE ---ABCDE ---ABCDE ---ABCDE -Content-Type: text/x-two - -Blah - ---ABCDE-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_38.txt b/src/libs/python/bongo/external/email/test/data/msg_38.txt deleted file mode 100644 index 006df81..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_38.txt +++ /dev/null @@ -1,101 +0,0 @@ -MIME-Version: 1.0 -Content-Type: multipart/mixed; boundary="----- =_aaaaaaaaaa0" - -------- =_aaaaaaaaaa0 -Content-Type: multipart/mixed; boundary="----- =_aaaaaaaaaa1" -Content-ID: <20592.1022586929.1@example.com> - -------- =_aaaaaaaaaa1 -Content-Type: multipart/alternative; boundary="----- =_aaaaaaaaaa2" -Content-ID: <20592.1022586929.2@example.com> - -------- =_aaaaaaaaaa2 -Content-Type: text/plain -Content-ID: <20592.1022586929.3@example.com> -Content-Description: very tricky -Content-Transfer-Encoding: 7bit - - -Unlike the test test_nested-multiples-with-internal-boundary, this -piece of text not only contains the outer boundary tags -------- =_aaaaaaaaaa1 -and -------- =_aaaaaaaaaa0 -but puts them at the start of a line! And, to be even nastier, it -even includes a couple of end tags, such as this one: - -------- =_aaaaaaaaaa1-- - -and this one, which is from a multipart we haven't even seen yet! - -------- =_aaaaaaaaaa4-- - -This will, I'm sure, cause much breakage of MIME parsers. But, as -far as I can tell, it's perfectly legal. I have not yet ever seen -a case of this in the wild, but I've seen *similar* things. - - -------- =_aaaaaaaaaa2 -Content-Type: application/octet-stream -Content-ID: <20592.1022586929.4@example.com> -Content-Description: patch2 -Content-Transfer-Encoding: base64 - -XXX - -------- =_aaaaaaaaaa2-- - -------- =_aaaaaaaaaa1 -Content-Type: multipart/alternative; boundary="----- =_aaaaaaaaaa3" -Content-ID: <20592.1022586929.6@example.com> - -------- =_aaaaaaaaaa3 -Content-Type: application/octet-stream -Content-ID: <20592.1022586929.7@example.com> -Content-Description: patch3 -Content-Transfer-Encoding: base64 - -XXX - -------- =_aaaaaaaaaa3 -Content-Type: application/octet-stream -Content-ID: <20592.1022586929.8@example.com> -Content-Description: patch4 -Content-Transfer-Encoding: base64 - -XXX - -------- =_aaaaaaaaaa3-- - -------- =_aaaaaaaaaa1 -Content-Type: multipart/alternative; boundary="----- =_aaaaaaaaaa4" -Content-ID: <20592.1022586929.10@example.com> - -------- =_aaaaaaaaaa4 -Content-Type: application/octet-stream -Content-ID: <20592.1022586929.11@example.com> -Content-Description: patch5 -Content-Transfer-Encoding: base64 - -XXX - -------- =_aaaaaaaaaa4 -Content-Type: application/octet-stream -Content-ID: <20592.1022586929.12@example.com> -Content-Description: patch6 -Content-Transfer-Encoding: base64 - -XXX - -------- =_aaaaaaaaaa4-- - -------- =_aaaaaaaaaa1-- - -------- =_aaaaaaaaaa0 -Content-Type: text/plain; charset="us-ascii" -Content-ID: <20592.1022586929.15@example.com> - --- -It's never too late to have a happy childhood. - -------- =_aaaaaaaaaa0-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_39.txt b/src/libs/python/bongo/external/email/test/data/msg_39.txt deleted file mode 100644 index 124b269..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_39.txt +++ /dev/null @@ -1,83 +0,0 @@ -MIME-Version: 1.0 -Content-Type: multipart/mixed; boundary="----- =_aaaaaaaaaa0" - -------- =_aaaaaaaaaa0 -Content-Type: multipart/mixed; boundary="----- =_aaaaaaaaaa1" -Content-ID: <20592.1022586929.1@example.com> - -------- =_aaaaaaaaaa1 -Content-Type: multipart/alternative; boundary="----- =_aaaaaaaaaa1" -Content-ID: <20592.1022586929.2@example.com> - -------- =_aaaaaaaaaa1 -Content-Type: application/octet-stream -Content-ID: <20592.1022586929.3@example.com> -Content-Description: patch1 -Content-Transfer-Encoding: base64 - -XXX - -------- =_aaaaaaaaaa1 -Content-Type: application/octet-stream -Content-ID: <20592.1022586929.4@example.com> -Content-Description: patch2 -Content-Transfer-Encoding: base64 - -XXX - -------- =_aaaaaaaaaa1-- - -------- =_aaaaaaaaaa1 -Content-Type: multipart/alternative; boundary="----- =_aaaaaaaaaa1" -Content-ID: <20592.1022586929.6@example.com> - -------- =_aaaaaaaaaa1 -Content-Type: application/octet-stream -Content-ID: <20592.1022586929.7@example.com> -Content-Description: patch3 -Content-Transfer-Encoding: base64 - -XXX - -------- =_aaaaaaaaaa1 -Content-Type: application/octet-stream -Content-ID: <20592.1022586929.8@example.com> -Content-Description: patch4 -Content-Transfer-Encoding: base64 - -XXX - -------- =_aaaaaaaaaa1-- - -------- =_aaaaaaaaaa1 -Content-Type: multipart/alternative; boundary="----- =_aaaaaaaaaa1" -Content-ID: <20592.1022586929.10@example.com> - -------- =_aaaaaaaaaa1 -Content-Type: application/octet-stream -Content-ID: <20592.1022586929.11@example.com> -Content-Description: patch5 -Content-Transfer-Encoding: base64 - -XXX - -------- =_aaaaaaaaaa1 -Content-Type: application/octet-stream -Content-ID: <20592.1022586929.12@example.com> -Content-Description: patch6 -Content-Transfer-Encoding: base64 - -XXX - -------- =_aaaaaaaaaa1-- - -------- =_aaaaaaaaaa1-- - -------- =_aaaaaaaaaa0 -Content-Type: text/plain; charset="us-ascii" -Content-ID: <20592.1022586929.15@example.com> - --- -It's never too late to have a happy childhood. - -------- =_aaaaaaaaaa0-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_40.txt b/src/libs/python/bongo/external/email/test/data/msg_40.txt deleted file mode 100644 index 1435fa1..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_40.txt +++ /dev/null @@ -1,10 +0,0 @@ -MIME-Version: 1.0 -Content-Type: text/html; boundary="--961284236552522269" - -----961284236552522269 -Content-Type: text/html; -Content-Transfer-Encoding: 7Bit - - - -----961284236552522269-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_41.txt b/src/libs/python/bongo/external/email/test/data/msg_41.txt deleted file mode 100644 index 76cdd1c..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_41.txt +++ /dev/null @@ -1,8 +0,0 @@ -From: "Allison Dunlap" -To: yyy@example.com -Subject: 64423 -Date: Sun, 11 Jul 2004 16:09:27 -0300 -MIME-Version: 1.0 -Content-Type: multipart/alternative; - -Blah blah blah diff --git a/src/libs/python/bongo/external/email/test/data/msg_42.txt b/src/libs/python/bongo/external/email/test/data/msg_42.txt deleted file mode 100644 index a75f8f4..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_42.txt +++ /dev/null @@ -1,20 +0,0 @@ -Content-Type: multipart/mixed; boundary="AAA" -From: Mail Delivery Subsystem -To: yyy@example.com - -This is a MIME-encapsulated message - ---AAA - -Stuff - ---AAA -Content-Type: message/rfc822 - -From: webmaster@python.org -To: zzz@example.com -Content-Type: multipart/mixed; boundary="BBB" - ---BBB-- - ---AAA-- diff --git a/src/libs/python/bongo/external/email/test/data/msg_43.txt b/src/libs/python/bongo/external/email/test/data/msg_43.txt deleted file mode 100644 index 797d12c..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_43.txt +++ /dev/null @@ -1,217 +0,0 @@ -From SRS0=aO/p=ON=bag.python.org=None@bounce2.pobox.com Fri Nov 26 21:40:36 2004 -X-VM-v5-Data: ([nil nil nil nil nil nil nil nil nil] - [nil nil nil nil nil nil nil "MAILER DAEMON <>" "MAILER DAEMON <>" nil nil "Banned file: auto__mail.python.bat in mail from you" "^From:" nil nil nil nil "Banned file: auto__mail.python.bat in mail from you" nil nil nil nil nil nil nil] - nil) -MIME-Version: 1.0 -Message-Id: -Content-Type: multipart/report; report-type=delivery-status; - charset=utf-8; - boundary="----------=_1101526904-1956-5" -X-Virus-Scanned: by XS4ALL Virus Scanner -X-UIDL: 4\G!!! -To: -Subject: Banned file: auto__mail.python.bat in mail from you -Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -This is a multi-part message in MIME format... - -------------=_1101526904-1956-5 -Content-Type: text/plain; charset="utf-8" -Content-Disposition: inline -Content-Transfer-Encoding: 7bit - -BANNED FILENAME ALERT - -Your message to: xxxxxxx@dot.ca.gov, xxxxxxxxxxxxx@dot.ca.gov, xxxxxxxxxx@dot.ca.gov, xxxxxxxx@dot.ca.gov, xxxxxxxxxx@dot.ca.gov, xxxxxx@dot.ca.gov, xxxxxxxxxx@dot.ca.gov, xxxxxx@dot.ca.gov, xxxxxx@dot.ca.gov, xxxxxxxxxxxxxxxx@dot.ca.gov, xxxxxxxxxxx@dot.ca.gov, xxxxxxxxxx@dot.ca.gov, xxxxxxxxxx@dot.ca.gov, xxxxxxxxxxxx@dot.ca.gov, xxxxxxxxxxxx@dot.ca.gov, xxxxxxx@dot.ca.gov, xxxxxxxxx@dot.ca.gov, xxxxxxxxxx@dot.ca.gov, xxxxxx@dot.ca.gov, xxx@dot.ca.gov, xxxxxxx@dot.ca.gov, xxxxxxx@dot.ca.gov, xxxxxxxxxxxxxxx@dot.ca.gov, xxxxxxxxxx@dot.ca.gov, xxxxxxx@dot.ca.gov, xxx@dot.ca.gov, xxxxxxxx@dot.ca.gov, xxxxxxxxxxxxx@dot.ca.gov, xxxxxxxxxxxxx@dot.ca.gov, xxxxxxxxxxx@dot.ca.gov, xxxxxxxxx@dot.ca.gov, xxxxxxxxxx@dot.ca.gov, xxxxxxxxxxxx@dot.ca.gov, xxxxxxx@dot.ca.gov, xxxxxxxxxxxxxxx@dot.ca.gov, xxxxxxxxxxxxx@dot.ca.gov, xxxx@dot.ca.gov, xxxxxxxx@dot.ca.gov, xxxxxxxxxx@dot.ca.gov, xxxxxxxxxxxxxxxxxx@dot.ca.gov -was blocked by our Spam Firewall. The email you sent with the following subject has NOT BEEN DELIVERED: - -Subject: Delivery_failure_notice - -An attachment in that mail was of a file type that the Spam Firewall is set to block. - - - -------------=_1101526904-1956-5 -Content-Type: message/delivery-status -Content-Disposition: inline -Content-Transfer-Encoding: 7bit -Content-Description: Delivery error report - -Reporting-MTA: dns; sacspam01.dot.ca.gov -Received-From-MTA: smtp; sacspam01.dot.ca.gov ([127.0.0.1]) -Arrival-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxxxxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxxxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -Final-Recipient: rfc822; xxxxxxx@dot.ca.gov -Action: failed -Status: 5.7.1 -Diagnostic-Code: smtp; 550 5.7.1 Message content rejected, id=01956-02-2 - BANNED: auto__mail.python.bat -Last-Attempt-Date: Fri, 26 Nov 2004 19:41:44 -0800 (PST) - -------------=_1101526904-1956-5 -Content-Type: text/rfc822-headers -Content-Disposition: inline -Content-Transfer-Encoding: 7bit -Content-Description: Undelivered-message headers - -Received: from kgsav.org (ppp-70-242-162-63.dsl.spfdmo.swbell.net [70.242.162.63]) - by sacspam01.dot.ca.gov (Spam Firewall) with SMTP - id A232AD03DE3A; Fri, 26 Nov 2004 19:41:35 -0800 (PST) -From: webmaster@python.org -To: xxxxx@dot.ca.gov -Date: Sat, 27 Nov 2004 03:35:30 UTC -Subject: Delivery_failure_notice -Importance: Normal -X-Priority: 3 (Normal) -X-MSMail-Priority: Normal -Message-ID: -MIME-Version: 1.0 -Content-Type: multipart/mixed; boundary="====67bd2b7a5.f99f7" -Content-Transfer-Encoding: 7bit - -------------=_1101526904-1956-5-- - diff --git a/src/libs/python/bongo/external/email/test/data/msg_44.txt b/src/libs/python/bongo/external/email/test/data/msg_44.txt deleted file mode 100644 index ae462a6..0000000 --- a/src/libs/python/bongo/external/email/test/data/msg_44.txt +++ /dev/null @@ -1,35 +0,0 @@ -Return-Path: -Delivered-To: barry@python.org -Received: by mail.python.org (Postfix, from userid 889) - id C2BF0D37C6; Tue, 11 Sep 2001 00:05:05 -0400 (EDT) -MIME-Version: 1.0 -Content-Type: multipart/mixed; boundary="h90VIIIKmx" -Content-Transfer-Encoding: 7bit -Message-ID: <15261.36209.358846.118674@anthem.python.org> -From: barry@python.org (Barry A. Warsaw) -To: barry@python.org -Subject: a simple multipart -Date: Tue, 11 Sep 2001 00:05:05 -0400 -X-Mailer: VM 6.95 under 21.4 (patch 4) "Artificial Intelligence" XEmacs Lucid -X-Attribution: BAW -X-Oblique-Strategy: Make a door into a window - - ---h90VIIIKmx -Content-Type: text/plain -Content-Disposition: inline; name="msg.txt" -Content-Transfer-Encoding: 7bit - -a simple kind of mirror -to reflect upon our own - ---h90VIIIKmx -Content-Type: text/plain -Content-Disposition: inline; name="msg.txt" -Content-Transfer-Encoding: 7bit - -a simple kind of mirror -to reflect upon our own - ---h90VIIIKmx-- - diff --git a/src/libs/python/bongo/external/email/test/test_email.py b/src/libs/python/bongo/external/email/test/test_email.py deleted file mode 100644 index 3a873e9..0000000 --- a/src/libs/python/bongo/external/email/test/test_email.py +++ /dev/null @@ -1,3252 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Contact: email-sig@python.org -# email package unit tests - -import os -import sys -import time -import base64 -import difflib -import unittest -import warnings -from io import StringIO - -import bongo.external.email - -from bongo.external.email.charset import Charset -from bongo.external.email.header import Header, decode_header, make_header -from bongo.external.email.parser import Parser, HeaderParser -from bongo.external.email.generator import Generator, DecodedGenerator -from bongo.external.email.message import Message -from bongo.external.email.mime.audio import MIMEAudio -from bongo.external.email.mime.text import MIMEText -from bongo.external.email.mime.image import MIMEImage -from bongo.external.email.mime.base import MIMEBase -from bongo.external.email.mime.message import MIMEMessage -from bongo.external.email.mime.multipart import MIMEMultipart -from bongo.external.email import Utils -from bongo.external.email import Errors -from bongo.external.email import Encoders -from bongo.external.email import Iterators -from bongo.external.email import base64MIME -from bongo.external.email import quopriMIME - -from test.test_support import findfile, run_unittest -from bongo.external.email.test import __file__ as landmark - - -NL = '\n' -EMPTYSTRING = '' -SPACE = ' ' - - - -def openfile(filename, mode='r'): - path = os.path.join(os.path.dirname(landmark), 'data', filename) - return open(path, mode) - - - -# Base test class -class TestEmailBase(unittest.TestCase): - def ndiffAssertEqual(self, first, second): - """Like failUnlessEqual except use ndiff for readable output.""" - if first != second: - sfirst = str(first) - ssecond = str(second) - diff = difflib.ndiff(sfirst.splitlines(), ssecond.splitlines()) - fp = StringIO() - print(NL, NL.join(diff), file=fp) - raise self.failureException(fp.getvalue()) - - def _msgobj(self, filename): - fp = openfile(findfile(filename)) - try: - msg = bongo.external.email.message_from_file(fp) - finally: - fp.close() - return msg - - - -# Test various aspects of the Message class's API -class TestMessageAPI(TestEmailBase): - def test_get_all(self): - eq = self.assertEqual - msg = self._msgobj('msg_20.txt') - eq(msg.get_all('cc'), ['ccc@zzz.org', 'ddd@zzz.org', 'eee@zzz.org']) - eq(msg.get_all('xx', 'n/a'), 'n/a') - - def test_getset_charset(self): - eq = self.assertEqual - msg = Message() - eq(msg.get_charset(), None) - charset = Charset('iso-8859-1') - msg.set_charset(charset) - eq(msg['mime-version'], '1.0') - eq(msg.get_content_type(), 'text/plain') - eq(msg['content-type'], 'text/plain; charset="iso-8859-1"') - eq(msg.get_param('charset'), 'iso-8859-1') - eq(msg['content-transfer-encoding'], 'quoted-printable') - eq(msg.get_charset().input_charset, 'iso-8859-1') - # Remove the charset - msg.set_charset(None) - eq(msg.get_charset(), None) - eq(msg['content-type'], 'text/plain') - # Try adding a charset when there's already MIME headers present - msg = Message() - msg['MIME-Version'] = '2.0' - msg['Content-Type'] = 'text/x-weird' - msg['Content-Transfer-Encoding'] = 'quinted-puntable' - msg.set_charset(charset) - eq(msg['mime-version'], '2.0') - eq(msg['content-type'], 'text/x-weird; charset="iso-8859-1"') - eq(msg['content-transfer-encoding'], 'quinted-puntable') - - def test_set_charset_from_string(self): - eq = self.assertEqual - msg = Message() - msg.set_charset('us-ascii') - eq(msg.get_charset().input_charset, 'us-ascii') - eq(msg['content-type'], 'text/plain; charset="us-ascii"') - - def test_set_payload_with_charset(self): - msg = Message() - charset = Charset('iso-8859-1') - msg.set_payload('This is a string payload', charset) - self.assertEqual(msg.get_charset().input_charset, 'iso-8859-1') - - def test_get_charsets(self): - eq = self.assertEqual - - msg = self._msgobj('msg_08.txt') - charsets = msg.get_charsets() - eq(charsets, [None, 'us-ascii', 'iso-8859-1', 'iso-8859-2', 'koi8-r']) - - msg = self._msgobj('msg_09.txt') - charsets = msg.get_charsets('dingbat') - eq(charsets, ['dingbat', 'us-ascii', 'iso-8859-1', 'dingbat', - 'koi8-r']) - - msg = self._msgobj('msg_12.txt') - charsets = msg.get_charsets() - eq(charsets, [None, 'us-ascii', 'iso-8859-1', None, 'iso-8859-2', - 'iso-8859-3', 'us-ascii', 'koi8-r']) - - def test_get_filename(self): - eq = self.assertEqual - - msg = self._msgobj('msg_04.txt') - filenames = [p.get_filename() for p in msg.get_payload()] - eq(filenames, ['msg.txt', 'msg.txt']) - - msg = self._msgobj('msg_07.txt') - subpart = msg.get_payload(1) - eq(subpart.get_filename(), 'dingusfish.gif') - - def test_get_filename_with_name_parameter(self): - eq = self.assertEqual - - msg = self._msgobj('msg_44.txt') - filenames = [p.get_filename() for p in msg.get_payload()] - eq(filenames, ['msg.txt', 'msg.txt']) - - def test_get_boundary(self): - eq = self.assertEqual - msg = self._msgobj('msg_07.txt') - # No quotes! - eq(msg.get_boundary(), 'BOUNDARY') - - def test_set_boundary(self): - eq = self.assertEqual - # This one has no existing boundary parameter, but the Content-Type: - # header appears fifth. - msg = self._msgobj('msg_01.txt') - msg.set_boundary('BOUNDARY') - header, value = list(msg.items())[4] - eq(header.lower(), 'content-type') - eq(value, 'text/plain; charset="us-ascii"; boundary="BOUNDARY"') - # This one has a Content-Type: header, with a boundary, stuck in the - # middle of its headers. Make sure the order is preserved; it should - # be fifth. - msg = self._msgobj('msg_04.txt') - msg.set_boundary('BOUNDARY') - header, value = list(msg.items())[4] - eq(header.lower(), 'content-type') - eq(value, 'multipart/mixed; boundary="BOUNDARY"') - # And this one has no Content-Type: header at all. - msg = self._msgobj('msg_03.txt') - self.assertRaises(Errors.HeaderParseError, - msg.set_boundary, 'BOUNDARY') - - def test_get_decoded_payload(self): - eq = self.assertEqual - msg = self._msgobj('msg_10.txt') - # The outer message is a multipart - eq(msg.get_payload(decode=True), None) - # Subpart 1 is 7bit encoded - eq(msg.get_payload(0).get_payload(decode=True), - 'This is a 7bit encoded message.\n') - # Subpart 2 is quopri - eq(msg.get_payload(1).get_payload(decode=True), - '\xa1This is a Quoted Printable encoded message!\n') - # Subpart 3 is base64 - eq(msg.get_payload(2).get_payload(decode=True), - 'This is a Base64 encoded message.') - # Subpart 4 has no Content-Transfer-Encoding: header. - eq(msg.get_payload(3).get_payload(decode=True), - 'This has no Content-Transfer-Encoding: header.\n') - - def test_get_decoded_uu_payload(self): - eq = self.assertEqual - msg = Message() - msg.set_payload('begin 666 -\n+:&5L;&\\@=V]R;&0 \n \nend\n') - for cte in ('x-uuencode', 'uuencode', 'uue', 'x-uue'): - msg['content-transfer-encoding'] = cte - eq(msg.get_payload(decode=True), 'hello world') - # Now try some bogus data - msg.set_payload('foo') - eq(msg.get_payload(decode=True), 'foo') - - def test_decode_bogus_uu_payload_quietly(self): - msg = Message() - msg.set_payload('begin 664 foo.txt\n%' % i for i in range(10)]) - msg.set_payload('Test') - sfp = StringIO() - g = Generator(sfp) - g.flatten(msg) - eq(sfp.getvalue(), """\ -From: test@dom.ain -References: <0@dom.ain> <1@dom.ain> <2@dom.ain> <3@dom.ain> <4@dom.ain> -\t<5@dom.ain> <6@dom.ain> <7@dom.ain> <8@dom.ain> <9@dom.ain> - -Test""") - - def test_no_split_long_header(self): - eq = self.ndiffAssertEqual - hstr = 'References: ' + 'x' * 80 - h = Header(hstr, continuation_ws='\t') - eq(h.encode(), """\ -References: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx""") - - def test_splitting_multiple_long_lines(self): - eq = self.ndiffAssertEqual - hstr = """\ -from babylon.socal-raves.org (localhost [127.0.0.1]); by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; for ; Sat, 2 Feb 2002 17:00:06 -0800 (PST) -\tfrom babylon.socal-raves.org (localhost [127.0.0.1]); by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; for ; Sat, 2 Feb 2002 17:00:06 -0800 (PST) -\tfrom babylon.socal-raves.org (localhost [127.0.0.1]); by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; for ; Sat, 2 Feb 2002 17:00:06 -0800 (PST) -""" - h = Header(hstr, continuation_ws='\t') - eq(h.encode(), """\ -from babylon.socal-raves.org (localhost [127.0.0.1]); -\tby babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; -\tfor ; -\tSat, 2 Feb 2002 17:00:06 -0800 (PST) -\tfrom babylon.socal-raves.org (localhost [127.0.0.1]); -\tby babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; -\tfor ; -\tSat, 2 Feb 2002 17:00:06 -0800 (PST) -\tfrom babylon.socal-raves.org (localhost [127.0.0.1]); -\tby babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; -\tfor ; -\tSat, 2 Feb 2002 17:00:06 -0800 (PST)""") - - def test_splitting_first_line_only_is_long(self): - eq = self.ndiffAssertEqual - hstr = """\ -from modemcable093.139-201-24.que.mc.videotron.ca ([24.201.139.93] helo=cthulhu.gerg.ca) -\tby kronos.mems-exchange.org with esmtp (Exim 4.05) -\tid 17k4h5-00034i-00 -\tfor test@mems-exchange.org; Wed, 28 Aug 2002 11:25:20 -0400""" - h = Header(hstr, maxlinelen=78, header_name='Received', - continuation_ws='\t') - eq(h.encode(), """\ -from modemcable093.139-201-24.que.mc.videotron.ca ([24.201.139.93] -\thelo=cthulhu.gerg.ca) -\tby kronos.mems-exchange.org with esmtp (Exim 4.05) -\tid 17k4h5-00034i-00 -\tfor test@mems-exchange.org; Wed, 28 Aug 2002 11:25:20 -0400""") - - def test_long_8bit_header(self): - eq = self.ndiffAssertEqual - msg = Message() - h = Header('Britische Regierung gibt', 'iso-8859-1', - header_name='Subject') - h.append('gr\xfcnes Licht f\xfcr Offshore-Windkraftprojekte') - msg['Subject'] = h - eq(msg.as_string(), """\ -Subject: =?iso-8859-1?q?Britische_Regierung_gibt?= =?iso-8859-1?q?gr=FCnes?= - =?iso-8859-1?q?_Licht_f=FCr_Offshore-Windkraftprojekte?= - -""") - - def test_long_8bit_header_no_charset(self): - eq = self.ndiffAssertEqual - msg = Message() - msg['Reply-To'] = 'Britische Regierung gibt gr\xfcnes Licht f\xfcr Offshore-Windkraftprojekte ' - eq(msg.as_string(), """\ -Reply-To: Britische Regierung gibt gr\xfcnes Licht f\xfcr Offshore-Windkraftprojekte - -""") - - def test_long_to_header(self): - eq = self.ndiffAssertEqual - to = '"Someone Test #A" ,,"Someone Test #B" , "Someone Test #C" , "Someone Test #D" ' - msg = Message() - msg['To'] = to - eq(msg.as_string(0), '''\ -To: "Someone Test #A" , , -\t"Someone Test #B" , -\t"Someone Test #C" , -\t"Someone Test #D" - -''') - - def test_long_line_after_append(self): - eq = self.ndiffAssertEqual - s = 'This is an example of string which has almost the limit of header length.' - h = Header(s) - h.append('Add another line.') - eq(h.encode(), """\ -This is an example of string which has almost the limit of header length. - Add another line.""") - - def test_shorter_line_with_append(self): - eq = self.ndiffAssertEqual - s = 'This is a shorter line.' - h = Header(s) - h.append('Add another sentence. (Surprise?)') - eq(h.encode(), - 'This is a shorter line. Add another sentence. (Surprise?)') - - def test_long_field_name(self): - eq = self.ndiffAssertEqual - fn = 'X-Very-Very-Very-Long-Header-Name' - gs = "Die Mieter treten hier ein werden mit einem Foerderband komfortabel den Korridor entlang, an s\xfcdl\xfcndischen Wandgem\xe4lden vorbei, gegen die rotierenden Klingen bef\xf6rdert. " - h = Header(gs, 'iso-8859-1', header_name=fn) - # BAW: this seems broken because the first line is too long - eq(h.encode(), """\ -=?iso-8859-1?q?Die_Mieter_treten_hier_?= - =?iso-8859-1?q?ein_werden_mit_einem_Foerderband_komfortabel_den_Korridor_?= - =?iso-8859-1?q?entlang=2C_an_s=FCdl=FCndischen_Wandgem=E4lden_vorbei=2C_g?= - =?iso-8859-1?q?egen_die_rotierenden_Klingen_bef=F6rdert=2E_?=""") - - def test_long_received_header(self): - h = 'from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) by hrothgar.la.mastaler.com (tmda-ofmipd) with ESMTP; Wed, 05 Mar 2003 18:10:18 -0700' - msg = Message() - msg['Received-1'] = Header(h, continuation_ws='\t') - msg['Received-2'] = h - self.assertEqual(msg.as_string(), """\ -Received-1: from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) by -\throthgar.la.mastaler.com (tmda-ofmipd) with ESMTP; -\tWed, 05 Mar 2003 18:10:18 -0700 -Received-2: from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) by -\throthgar.la.mastaler.com (tmda-ofmipd) with ESMTP; -\tWed, 05 Mar 2003 18:10:18 -0700 - -""") - - def test_string_headerinst_eq(self): - h = '<15975.17901.207240.414604@sgigritzmann1.mathematik.tu-muenchen.de> (David Bremner\'s message of "Thu, 6 Mar 2003 13:58:21 +0100")' - msg = Message() - msg['Received-1'] = Header(h, header_name='Received-1', - continuation_ws='\t') - msg['Received-2'] = h - self.assertEqual(msg.as_string(), """\ -Received-1: <15975.17901.207240.414604@sgigritzmann1.mathematik.tu-muenchen.de> -\t(David Bremner's message of "Thu, 6 Mar 2003 13:58:21 +0100") -Received-2: <15975.17901.207240.414604@sgigritzmann1.mathematik.tu-muenchen.de> -\t(David Bremner's message of "Thu, 6 Mar 2003 13:58:21 +0100") - -""") - - def test_long_unbreakable_lines_with_continuation(self): - eq = self.ndiffAssertEqual - msg = Message() - t = """\ - iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9 - locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp""" - msg['Face-1'] = t - msg['Face-2'] = Header(t, header_name='Face-2') - eq(msg.as_string(), """\ -Face-1: iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9 -\tlocQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp -Face-2: iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9 - locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp - -""") - - def test_another_long_multiline_header(self): - eq = self.ndiffAssertEqual - m = '''\ -Received: from siimage.com ([172.25.1.3]) by zima.siliconimage.com with Microsoft SMTPSVC(5.0.2195.4905); -\tWed, 16 Oct 2002 07:41:11 -0700''' - msg = bongo.external.email.message_from_string(m) - eq(msg.as_string(), '''\ -Received: from siimage.com ([172.25.1.3]) by zima.siliconimage.com with -\tMicrosoft SMTPSVC(5.0.2195.4905); Wed, 16 Oct 2002 07:41:11 -0700 - -''') - - def test_long_lines_with_different_header(self): - eq = self.ndiffAssertEqual - h = """\ -List-Unsubscribe: , - """ - msg = Message() - msg['List'] = h - msg['List'] = Header(h, header_name='List') - eq(msg.as_string(), """\ -List: List-Unsubscribe: , -\t -List: List-Unsubscribe: , - - -""") - - - -# Test mangling of "From " lines in the body of a message -class TestFromMangling(unittest.TestCase): - def setUp(self): - self.msg = Message() - self.msg['From'] = 'aaa@bbb.org' - self.msg.set_payload("""\ -From the desk of A.A.A.: -Blah blah blah -""") - - def test_mangled_from(self): - s = StringIO() - g = Generator(s, mangle_from_=True) - g.flatten(self.msg) - self.assertEqual(s.getvalue(), """\ -From: aaa@bbb.org - ->From the desk of A.A.A.: -Blah blah blah -""") - - def test_dont_mangle_from(self): - s = StringIO() - g = Generator(s, mangle_from_=False) - g.flatten(self.msg) - self.assertEqual(s.getvalue(), """\ -From: aaa@bbb.org - -From the desk of A.A.A.: -Blah blah blah -""") - - - -# Test the basic MIMEAudio class -class TestMIMEAudio(unittest.TestCase): - def setUp(self): - # Make sure we pick up the audiotest.au that lives in email/test/data. - # In Python, there's an audiotest.au living in Lib/test but that isn't - # included in some binary distros that don't include the test - # package. The trailing empty string on the .join() is significant - # since findfile() will do a dirname(). - datadir = os.path.join(os.path.dirname(landmark), 'data', '') - fp = open(findfile('audiotest.au', datadir), 'rb') - try: - self._audiodata = fp.read() - finally: - fp.close() - self._au = MIMEAudio(self._audiodata) - - def test_guess_minor_type(self): - self.assertEqual(self._au.get_content_type(), 'audio/basic') - - def test_encoding(self): - payload = self._au.get_payload() - self.assertEqual(base64.decodestring(payload), self._audiodata) - - def test_checkSetMinor(self): - au = MIMEAudio(self._audiodata, 'fish') - self.assertEqual(au.get_content_type(), 'audio/fish') - - def test_add_header(self): - eq = self.assertEqual - unless = self.assertTrue - self._au.add_header('Content-Disposition', 'attachment', - filename='audiotest.au') - eq(self._au['content-disposition'], - 'attachment; filename="audiotest.au"') - eq(self._au.get_params(header='content-disposition'), - [('attachment', ''), ('filename', 'audiotest.au')]) - eq(self._au.get_param('filename', header='content-disposition'), - 'audiotest.au') - missing = [] - eq(self._au.get_param('attachment', header='content-disposition'), '') - unless(self._au.get_param('foo', failobj=missing, - header='content-disposition') is missing) - # Try some missing stuff - unless(self._au.get_param('foobar', missing) is missing) - unless(self._au.get_param('attachment', missing, - header='foobar') is missing) - - - -# Test the basic MIMEImage class -class TestMIMEImage(unittest.TestCase): - def setUp(self): - fp = openfile('PyBanner048.gif') - try: - self._imgdata = fp.read() - finally: - fp.close() - self._im = MIMEImage(self._imgdata) - - def test_guess_minor_type(self): - self.assertEqual(self._im.get_content_type(), 'image/gif') - - def test_encoding(self): - payload = self._im.get_payload() - self.assertEqual(base64.decodestring(payload), self._imgdata) - - def test_checkSetMinor(self): - im = MIMEImage(self._imgdata, 'fish') - self.assertEqual(im.get_content_type(), 'image/fish') - - def test_add_header(self): - eq = self.assertEqual - unless = self.assertTrue - self._im.add_header('Content-Disposition', 'attachment', - filename='dingusfish.gif') - eq(self._im['content-disposition'], - 'attachment; filename="dingusfish.gif"') - eq(self._im.get_params(header='content-disposition'), - [('attachment', ''), ('filename', 'dingusfish.gif')]) - eq(self._im.get_param('filename', header='content-disposition'), - 'dingusfish.gif') - missing = [] - eq(self._im.get_param('attachment', header='content-disposition'), '') - unless(self._im.get_param('foo', failobj=missing, - header='content-disposition') is missing) - # Try some missing stuff - unless(self._im.get_param('foobar', missing) is missing) - unless(self._im.get_param('attachment', missing, - header='foobar') is missing) - - - -# Test the basic MIMEText class -class TestMIMEText(unittest.TestCase): - def setUp(self): - self._msg = MIMEText('hello there') - - def test_types(self): - eq = self.assertEqual - unless = self.assertTrue - eq(self._msg.get_content_type(), 'text/plain') - eq(self._msg.get_param('charset'), 'us-ascii') - missing = [] - unless(self._msg.get_param('foobar', missing) is missing) - unless(self._msg.get_param('charset', missing, header='foobar') - is missing) - - def test_payload(self): - self.assertEqual(self._msg.get_payload(), 'hello there') - self.assertTrue(not self._msg.is_multipart()) - - def test_charset(self): - eq = self.assertEqual - msg = MIMEText('hello there', _charset='us-ascii') - eq(msg.get_charset().input_charset, 'us-ascii') - eq(msg['content-type'], 'text/plain; charset="us-ascii"') - - - -# Test complicated multipart/* messages -class TestMultipart(TestEmailBase): - def setUp(self): - fp = openfile('PyBanner048.gif') - try: - data = fp.read() - finally: - fp.close() - - container = MIMEBase('multipart', 'mixed', boundary='BOUNDARY') - image = MIMEImage(data, name='dingusfish.gif') - image.add_header('content-disposition', 'attachment', - filename='dingusfish.gif') - intro = MIMEText('''\ -Hi there, - -This is the dingus fish. -''') - container.attach(intro) - container.attach(image) - container['From'] = 'Barry ' - container['To'] = 'Dingus Lovers ' - container['Subject'] = 'Here is your dingus fish' - - now = 987809702.54848599 - timetuple = time.localtime(now) - if timetuple[-1] == 0: - tzsecs = time.timezone - else: - tzsecs = time.altzone - if tzsecs > 0: - sign = '-' - else: - sign = '+' - tzoffset = ' %s%04d' % (sign, tzsecs / 36) - container['Date'] = time.strftime( - '%a, %d %b %Y %H:%M:%S', - time.localtime(now)) + tzoffset - self._msg = container - self._im = image - self._txt = intro - - def test_hierarchy(self): - # convenience - eq = self.assertEqual - unless = self.assertTrue - raises = self.assertRaises - # tests - m = self._msg - unless(m.is_multipart()) - eq(m.get_content_type(), 'multipart/mixed') - eq(len(m.get_payload()), 2) - raises(IndexError, m.get_payload, 2) - m0 = m.get_payload(0) - m1 = m.get_payload(1) - unless(m0 is self._txt) - unless(m1 is self._im) - eq(m.get_payload(), [m0, m1]) - unless(not m0.is_multipart()) - unless(not m1.is_multipart()) - - def test_empty_multipart_idempotent(self): - text = """\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - - ---BOUNDARY - - ---BOUNDARY-- -""" - msg = Parser().parsestr(text) - self.ndiffAssertEqual(text, msg.as_string()) - - def test_no_parts_in_a_multipart_with_none_epilogue(self): - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.set_boundary('BOUNDARY') - self.ndiffAssertEqual(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - ---BOUNDARY - ---BOUNDARY--''') - - def test_no_parts_in_a_multipart_with_empty_epilogue(self): - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.preamble = '' - outer.epilogue = '' - outer.set_boundary('BOUNDARY') - self.ndiffAssertEqual(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - - ---BOUNDARY - ---BOUNDARY-- -''') - - def test_one_part_in_a_multipart(self): - eq = self.ndiffAssertEqual - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.set_boundary('BOUNDARY') - msg = MIMEText('hello world') - outer.attach(msg) - eq(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -hello world ---BOUNDARY--''') - - def test_seq_parts_in_a_multipart_with_empty_preamble(self): - eq = self.ndiffAssertEqual - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.preamble = '' - msg = MIMEText('hello world') - outer.attach(msg) - outer.set_boundary('BOUNDARY') - eq(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -hello world ---BOUNDARY--''') - - - def test_seq_parts_in_a_multipart_with_none_preamble(self): - eq = self.ndiffAssertEqual - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.preamble = None - msg = MIMEText('hello world') - outer.attach(msg) - outer.set_boundary('BOUNDARY') - eq(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -hello world ---BOUNDARY--''') - - - def test_seq_parts_in_a_multipart_with_none_epilogue(self): - eq = self.ndiffAssertEqual - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.epilogue = None - msg = MIMEText('hello world') - outer.attach(msg) - outer.set_boundary('BOUNDARY') - eq(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -hello world ---BOUNDARY--''') - - - def test_seq_parts_in_a_multipart_with_empty_epilogue(self): - eq = self.ndiffAssertEqual - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.epilogue = '' - msg = MIMEText('hello world') - outer.attach(msg) - outer.set_boundary('BOUNDARY') - eq(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -hello world ---BOUNDARY-- -''') - - - def test_seq_parts_in_a_multipart_with_nl_epilogue(self): - eq = self.ndiffAssertEqual - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.epilogue = '\n' - msg = MIMEText('hello world') - outer.attach(msg) - outer.set_boundary('BOUNDARY') - eq(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -hello world ---BOUNDARY-- - -''') - - def test_message_external_body(self): - eq = self.assertEqual - msg = self._msgobj('msg_36.txt') - eq(len(msg.get_payload()), 2) - msg1 = msg.get_payload(1) - eq(msg1.get_content_type(), 'multipart/alternative') - eq(len(msg1.get_payload()), 2) - for subpart in msg1.get_payload(): - eq(subpart.get_content_type(), 'message/external-body') - eq(len(subpart.get_payload()), 1) - subsubpart = subpart.get_payload(0) - eq(subsubpart.get_content_type(), 'text/plain') - - def test_double_boundary(self): - # msg_37.txt is a multipart that contains two dash-boundary's in a - # row. Our interpretation of RFC 2046 calls for ignoring the second - # and subsequent boundaries. - msg = self._msgobj('msg_37.txt') - self.assertEqual(len(msg.get_payload()), 3) - - def test_nested_inner_contains_outer_boundary(self): - eq = self.ndiffAssertEqual - # msg_38.txt has an inner part that contains outer boundaries. My - # interpretation of RFC 2046 (based on sections 5.1 and 5.1.2) say - # these are illegal and should be interpreted as unterminated inner - # parts. - msg = self._msgobj('msg_38.txt') - sfp = StringIO() - Iterators._structure(msg, sfp) - eq(sfp.getvalue(), """\ -multipart/mixed - multipart/mixed - multipart/alternative - text/plain - text/plain - text/plain - text/plain -""") - - def test_nested_with_same_boundary(self): - eq = self.ndiffAssertEqual - # msg 39.txt is similarly evil in that it's got inner parts that use - # the same boundary as outer parts. Again, I believe the way this is - # parsed is closest to the spirit of RFC 2046 - msg = self._msgobj('msg_39.txt') - sfp = StringIO() - Iterators._structure(msg, sfp) - eq(sfp.getvalue(), """\ -multipart/mixed - multipart/mixed - multipart/alternative - application/octet-stream - application/octet-stream - text/plain -""") - - def test_boundary_in_non_multipart(self): - msg = self._msgobj('msg_40.txt') - self.assertEqual(msg.as_string(), '''\ -MIME-Version: 1.0 -Content-Type: text/html; boundary="--961284236552522269" - -----961284236552522269 -Content-Type: text/html; -Content-Transfer-Encoding: 7Bit - - - -----961284236552522269-- -''') - - def test_boundary_with_leading_space(self): - eq = self.assertEqual - msg = bongo.external.email.message_from_string('''\ -MIME-Version: 1.0 -Content-Type: multipart/mixed; boundary=" XXXX" - --- XXXX -Content-Type: text/plain - - --- XXXX -Content-Type: text/plain - --- XXXX-- -''') - self.assertTrue(msg.is_multipart()) - eq(msg.get_boundary(), ' XXXX') - eq(len(msg.get_payload()), 2) - - def test_boundary_without_trailing_newline(self): - m = Parser().parsestr("""\ -Content-Type: multipart/mixed; boundary="===============0012394164==" -MIME-Version: 1.0 - ---===============0012394164== -Content-Type: image/file1.jpg -MIME-Version: 1.0 -Content-Transfer-Encoding: base64 - -YXNkZg== ---===============0012394164==--""") - self.assertEqual(m.get_payload(0).get_payload(), 'YXNkZg==') - - - -# Test some badly formatted messages -class TestNonConformant(TestEmailBase): - def test_parse_missing_minor_type(self): - eq = self.assertEqual - msg = self._msgobj('msg_14.txt') - eq(msg.get_content_type(), 'text/plain') - eq(msg.get_content_maintype(), 'text') - eq(msg.get_content_subtype(), 'plain') - - def test_same_boundary_inner_outer(self): - unless = self.assertTrue - msg = self._msgobj('msg_15.txt') - # XXX We can probably eventually do better - inner = msg.get_payload(0) - unless(hasattr(inner, 'defects')) - self.assertEqual(len(inner.defects), 1) - unless(isinstance(inner.defects[0], - Errors.StartBoundaryNotFoundDefect)) - - def test_multipart_no_boundary(self): - unless = self.assertTrue - msg = self._msgobj('msg_25.txt') - unless(isinstance(msg.get_payload(), str)) - self.assertEqual(len(msg.defects), 2) - unless(isinstance(msg.defects[0], Errors.NoBoundaryInMultipartDefect)) - unless(isinstance(msg.defects[1], - Errors.MultipartInvariantViolationDefect)) - - def test_invalid_content_type(self): - eq = self.assertEqual - neq = self.ndiffAssertEqual - msg = Message() - # RFC 2045, $5.2 says invalid yields text/plain - msg['Content-Type'] = 'text' - eq(msg.get_content_maintype(), 'text') - eq(msg.get_content_subtype(), 'plain') - eq(msg.get_content_type(), 'text/plain') - # Clear the old value and try something /really/ invalid - del msg['content-type'] - msg['Content-Type'] = 'foo' - eq(msg.get_content_maintype(), 'text') - eq(msg.get_content_subtype(), 'plain') - eq(msg.get_content_type(), 'text/plain') - # Still, make sure that the message is idempotently generated - s = StringIO() - g = Generator(s) - g.flatten(msg) - neq(s.getvalue(), 'Content-Type: foo\n\n') - - def test_no_start_boundary(self): - eq = self.ndiffAssertEqual - msg = self._msgobj('msg_31.txt') - eq(msg.get_payload(), """\ ---BOUNDARY -Content-Type: text/plain - -message 1 - ---BOUNDARY -Content-Type: text/plain - -message 2 - ---BOUNDARY-- -""") - - def test_no_separating_blank_line(self): - eq = self.ndiffAssertEqual - msg = self._msgobj('msg_35.txt') - eq(msg.as_string(), """\ -From: aperson@dom.ain -To: bperson@dom.ain -Subject: here's something interesting - -counter to RFC 2822, there's no separating newline here -""") - - def test_lying_multipart(self): - unless = self.assertTrue - msg = self._msgobj('msg_41.txt') - unless(hasattr(msg, 'defects')) - self.assertEqual(len(msg.defects), 2) - unless(isinstance(msg.defects[0], Errors.NoBoundaryInMultipartDefect)) - unless(isinstance(msg.defects[1], - Errors.MultipartInvariantViolationDefect)) - - def test_missing_start_boundary(self): - outer = self._msgobj('msg_42.txt') - # The message structure is: - # - # multipart/mixed - # text/plain - # message/rfc822 - # multipart/mixed [*] - # - # [*] This message is missing its start boundary - bad = outer.get_payload(1).get_payload(0) - self.assertEqual(len(bad.defects), 1) - self.assertTrue(isinstance(bad.defects[0], - Errors.StartBoundaryNotFoundDefect)) - - - -# Test RFC 2047 header encoding and decoding -class TestRFC2047(unittest.TestCase): - def test_rfc2047_multiline(self): - eq = self.assertEqual - s = """Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz - foo bar =?mac-iceland?q?r=8Aksm=9Arg=8Cs?=""" - dh = decode_header(s) - eq(dh, [ - ('Re:', None), - ('r\x8aksm\x9arg\x8cs', 'mac-iceland'), - ('baz foo bar', None), - ('r\x8aksm\x9arg\x8cs', 'mac-iceland')]) - eq(str(make_header(dh)), - """Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz foo bar - =?mac-iceland?q?r=8Aksm=9Arg=8Cs?=""") - - def test_whitespace_eater_unicode(self): - eq = self.assertEqual - s = '=?ISO-8859-1?Q?Andr=E9?= Pirard ' - dh = decode_header(s) - eq(dh, [('Andr\xe9', 'iso-8859-1'), ('Pirard ', None)]) - hu = str(make_header(dh)).encode('latin-1') - eq(hu, 'Andr\xe9 Pirard ') - - def test_whitespace_eater_unicode_2(self): - eq = self.assertEqual - s = 'The =?iso-8859-1?b?cXVpY2sgYnJvd24gZm94?= jumped over the =?iso-8859-1?b?bGF6eSBkb2c=?=' - dh = decode_header(s) - eq(dh, [('The', None), ('quick brown fox', 'iso-8859-1'), - ('jumped over the', None), ('lazy dog', 'iso-8859-1')]) - hu = make_header(dh).__unicode__() - eq(hu, 'The quick brown fox jumped over the lazy dog') - - - -# Test the MIMEMessage class -class TestMIMEMessage(TestEmailBase): - def setUp(self): - fp = openfile('msg_11.txt') - try: - self._text = fp.read() - finally: - fp.close() - - def test_type_error(self): - self.assertRaises(TypeError, MIMEMessage, 'a plain string') - - def test_valid_argument(self): - eq = self.assertEqual - unless = self.assertTrue - subject = 'A sub-message' - m = Message() - m['Subject'] = subject - r = MIMEMessage(m) - eq(r.get_content_type(), 'message/rfc822') - payload = r.get_payload() - unless(isinstance(payload, list)) - eq(len(payload), 1) - subpart = payload[0] - unless(subpart is m) - eq(subpart['subject'], subject) - - def test_bad_multipart(self): - eq = self.assertEqual - msg1 = Message() - msg1['Subject'] = 'subpart 1' - msg2 = Message() - msg2['Subject'] = 'subpart 2' - r = MIMEMessage(msg1) - self.assertRaises(Errors.MultipartConversionError, r.attach, msg2) - - def test_generate(self): - # First craft the message to be encapsulated - m = Message() - m['Subject'] = 'An enclosed message' - m.set_payload('Here is the body of the message.\n') - r = MIMEMessage(m) - r['Subject'] = 'The enclosing message' - s = StringIO() - g = Generator(s) - g.flatten(r) - self.assertEqual(s.getvalue(), """\ -Content-Type: message/rfc822 -MIME-Version: 1.0 -Subject: The enclosing message - -Subject: An enclosed message - -Here is the body of the message. -""") - - def test_parse_message_rfc822(self): - eq = self.assertEqual - unless = self.assertTrue - msg = self._msgobj('msg_11.txt') - eq(msg.get_content_type(), 'message/rfc822') - payload = msg.get_payload() - unless(isinstance(payload, list)) - eq(len(payload), 1) - submsg = payload[0] - self.assertTrue(isinstance(submsg, Message)) - eq(submsg['subject'], 'An enclosed message') - eq(submsg.get_payload(), 'Here is the body of the message.\n') - - def test_dsn(self): - eq = self.assertEqual - unless = self.assertTrue - # msg 16 is a Delivery Status Notification, see RFC 1894 - msg = self._msgobj('msg_16.txt') - eq(msg.get_content_type(), 'multipart/report') - unless(msg.is_multipart()) - eq(len(msg.get_payload()), 3) - # Subpart 1 is a text/plain, human readable section - subpart = msg.get_payload(0) - eq(subpart.get_content_type(), 'text/plain') - eq(subpart.get_payload(), """\ -This report relates to a message you sent with the following header fields: - - Message-id: <002001c144a6$8752e060$56104586@oxy.edu> - Date: Sun, 23 Sep 2001 20:10:55 -0700 - From: "Ian T. Henry" - To: SoCal Raves - Subject: [scr] yeah for Ians!! - -Your message cannot be delivered to the following recipients: - - Recipient address: jangel1@cougar.noc.ucla.edu - Reason: recipient reached disk quota - -""") - # Subpart 2 contains the machine parsable DSN information. It - # consists of two blocks of headers, represented by two nested Message - # objects. - subpart = msg.get_payload(1) - eq(subpart.get_content_type(), 'message/delivery-status') - eq(len(subpart.get_payload()), 2) - # message/delivery-status should treat each block as a bunch of - # headers, i.e. a bunch of Message objects. - dsn1 = subpart.get_payload(0) - unless(isinstance(dsn1, Message)) - eq(dsn1['original-envelope-id'], '0GK500B4HD0888@cougar.noc.ucla.edu') - eq(dsn1.get_param('dns', header='reporting-mta'), '') - # Try a missing one - eq(dsn1.get_param('nsd', header='reporting-mta'), None) - dsn2 = subpart.get_payload(1) - unless(isinstance(dsn2, Message)) - eq(dsn2['action'], 'failed') - eq(dsn2.get_params(header='original-recipient'), - [('rfc822', ''), ('jangel1@cougar.noc.ucla.edu', '')]) - eq(dsn2.get_param('rfc822', header='final-recipient'), '') - # Subpart 3 is the original message - subpart = msg.get_payload(2) - eq(subpart.get_content_type(), 'message/rfc822') - payload = subpart.get_payload() - unless(isinstance(payload, list)) - eq(len(payload), 1) - subsubpart = payload[0] - unless(isinstance(subsubpart, Message)) - eq(subsubpart.get_content_type(), 'text/plain') - eq(subsubpart['message-id'], - '<002001c144a6$8752e060$56104586@oxy.edu>') - - def test_epilogue(self): - eq = self.ndiffAssertEqual - fp = openfile('msg_21.txt') - try: - text = fp.read() - finally: - fp.close() - msg = Message() - msg['From'] = 'aperson@dom.ain' - msg['To'] = 'bperson@dom.ain' - msg['Subject'] = 'Test' - msg.preamble = 'MIME message' - msg.epilogue = 'End of MIME message\n' - msg1 = MIMEText('One') - msg2 = MIMEText('Two') - msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY') - msg.attach(msg1) - msg.attach(msg2) - sfp = StringIO() - g = Generator(sfp) - g.flatten(msg) - eq(sfp.getvalue(), text) - - def test_no_nl_preamble(self): - eq = self.ndiffAssertEqual - msg = Message() - msg['From'] = 'aperson@dom.ain' - msg['To'] = 'bperson@dom.ain' - msg['Subject'] = 'Test' - msg.preamble = 'MIME message' - msg.epilogue = '' - msg1 = MIMEText('One') - msg2 = MIMEText('Two') - msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY') - msg.attach(msg1) - msg.attach(msg2) - eq(msg.as_string(), """\ -From: aperson@dom.ain -To: bperson@dom.ain -Subject: Test -Content-Type: multipart/mixed; boundary="BOUNDARY" - -MIME message ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -One ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -Two ---BOUNDARY-- -""") - - def test_default_type(self): - eq = self.assertEqual - fp = openfile('msg_30.txt') - try: - msg = bongo.external.email.message_from_file(fp) - finally: - fp.close() - container1 = msg.get_payload(0) - eq(container1.get_default_type(), 'message/rfc822') - eq(container1.get_content_type(), 'message/rfc822') - container2 = msg.get_payload(1) - eq(container2.get_default_type(), 'message/rfc822') - eq(container2.get_content_type(), 'message/rfc822') - container1a = container1.get_payload(0) - eq(container1a.get_default_type(), 'text/plain') - eq(container1a.get_content_type(), 'text/plain') - container2a = container2.get_payload(0) - eq(container2a.get_default_type(), 'text/plain') - eq(container2a.get_content_type(), 'text/plain') - - def test_default_type_with_explicit_container_type(self): - eq = self.assertEqual - fp = openfile('msg_28.txt') - try: - msg = bongo.external.email.message_from_file(fp) - finally: - fp.close() - container1 = msg.get_payload(0) - eq(container1.get_default_type(), 'message/rfc822') - eq(container1.get_content_type(), 'message/rfc822') - container2 = msg.get_payload(1) - eq(container2.get_default_type(), 'message/rfc822') - eq(container2.get_content_type(), 'message/rfc822') - container1a = container1.get_payload(0) - eq(container1a.get_default_type(), 'text/plain') - eq(container1a.get_content_type(), 'text/plain') - container2a = container2.get_payload(0) - eq(container2a.get_default_type(), 'text/plain') - eq(container2a.get_content_type(), 'text/plain') - - def test_default_type_non_parsed(self): - eq = self.assertEqual - neq = self.ndiffAssertEqual - # Set up container - container = MIMEMultipart('digest', 'BOUNDARY') - container.epilogue = '' - # Set up subparts - subpart1a = MIMEText('message 1\n') - subpart2a = MIMEText('message 2\n') - subpart1 = MIMEMessage(subpart1a) - subpart2 = MIMEMessage(subpart2a) - container.attach(subpart1) - container.attach(subpart2) - eq(subpart1.get_content_type(), 'message/rfc822') - eq(subpart1.get_default_type(), 'message/rfc822') - eq(subpart2.get_content_type(), 'message/rfc822') - eq(subpart2.get_default_type(), 'message/rfc822') - neq(container.as_string(0), '''\ -Content-Type: multipart/digest; boundary="BOUNDARY" -MIME-Version: 1.0 - ---BOUNDARY -Content-Type: message/rfc822 -MIME-Version: 1.0 - -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -message 1 - ---BOUNDARY -Content-Type: message/rfc822 -MIME-Version: 1.0 - -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -message 2 - ---BOUNDARY-- -''') - del subpart1['content-type'] - del subpart1['mime-version'] - del subpart2['content-type'] - del subpart2['mime-version'] - eq(subpart1.get_content_type(), 'message/rfc822') - eq(subpart1.get_default_type(), 'message/rfc822') - eq(subpart2.get_content_type(), 'message/rfc822') - eq(subpart2.get_default_type(), 'message/rfc822') - neq(container.as_string(0), '''\ -Content-Type: multipart/digest; boundary="BOUNDARY" -MIME-Version: 1.0 - ---BOUNDARY - -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -message 1 - ---BOUNDARY - -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -message 2 - ---BOUNDARY-- -''') - - def test_mime_attachments_in_constructor(self): - eq = self.assertEqual - text1 = MIMEText('') - text2 = MIMEText('') - msg = MIMEMultipart(_subparts=(text1, text2)) - eq(len(msg.get_payload()), 2) - eq(msg.get_payload(0), text1) - eq(msg.get_payload(1), text2) - - - -# A general test of parser->model->generator idempotency. IOW, read a message -# in, parse it into a message object tree, then without touching the tree, -# regenerate the plain text. The original text and the transformed text -# should be identical. Note: that we ignore the Unix-From since that may -# contain a changed date. -class TestIdempotent(TestEmailBase): - def _msgobj(self, filename): - fp = openfile(filename) - try: - data = fp.read() - finally: - fp.close() - msg = bongo.external.email.message_from_string(data) - return msg, data - - def _idempotent(self, msg, text): - eq = self.ndiffAssertEqual - s = StringIO() - g = Generator(s, maxheaderlen=0) - g.flatten(msg) - eq(text, s.getvalue()) - - def test_parse_text_message(self): - eq = self.assertEqual - msg, text = self._msgobj('msg_01.txt') - eq(msg.get_content_type(), 'text/plain') - eq(msg.get_content_maintype(), 'text') - eq(msg.get_content_subtype(), 'plain') - eq(msg.get_params()[1], ('charset', 'us-ascii')) - eq(msg.get_param('charset'), 'us-ascii') - eq(msg.preamble, None) - eq(msg.epilogue, None) - self._idempotent(msg, text) - - def test_parse_untyped_message(self): - eq = self.assertEqual - msg, text = self._msgobj('msg_03.txt') - eq(msg.get_content_type(), 'text/plain') - eq(msg.get_params(), None) - eq(msg.get_param('charset'), None) - self._idempotent(msg, text) - - def test_simple_multipart(self): - msg, text = self._msgobj('msg_04.txt') - self._idempotent(msg, text) - - def test_MIME_digest(self): - msg, text = self._msgobj('msg_02.txt') - self._idempotent(msg, text) - - def test_long_header(self): - msg, text = self._msgobj('msg_27.txt') - self._idempotent(msg, text) - - def test_MIME_digest_with_part_headers(self): - msg, text = self._msgobj('msg_28.txt') - self._idempotent(msg, text) - - def test_mixed_with_image(self): - msg, text = self._msgobj('msg_06.txt') - self._idempotent(msg, text) - - def test_multipart_report(self): - msg, text = self._msgobj('msg_05.txt') - self._idempotent(msg, text) - - def test_dsn(self): - msg, text = self._msgobj('msg_16.txt') - self._idempotent(msg, text) - - def test_preamble_epilogue(self): - msg, text = self._msgobj('msg_21.txt') - self._idempotent(msg, text) - - def test_multipart_one_part(self): - msg, text = self._msgobj('msg_23.txt') - self._idempotent(msg, text) - - def test_multipart_no_parts(self): - msg, text = self._msgobj('msg_24.txt') - self._idempotent(msg, text) - - def test_no_start_boundary(self): - msg, text = self._msgobj('msg_31.txt') - self._idempotent(msg, text) - - def test_rfc2231_charset(self): - msg, text = self._msgobj('msg_32.txt') - self._idempotent(msg, text) - - def test_more_rfc2231_parameters(self): - msg, text = self._msgobj('msg_33.txt') - self._idempotent(msg, text) - - def test_text_plain_in_a_multipart_digest(self): - msg, text = self._msgobj('msg_34.txt') - self._idempotent(msg, text) - - def test_nested_multipart_mixeds(self): - msg, text = self._msgobj('msg_12a.txt') - self._idempotent(msg, text) - - def test_message_external_body_idempotent(self): - msg, text = self._msgobj('msg_36.txt') - self._idempotent(msg, text) - - def test_content_type(self): - eq = self.assertEqual - unless = self.assertTrue - # Get a message object and reset the seek pointer for other tests - msg, text = self._msgobj('msg_05.txt') - eq(msg.get_content_type(), 'multipart/report') - # Test the Content-Type: parameters - params = {} - for pk, pv in msg.get_params(): - params[pk] = pv - eq(params['report-type'], 'delivery-status') - eq(params['boundary'], 'D1690A7AC1.996856090/mail.example.com') - eq(msg.preamble, 'This is a MIME-encapsulated message.\n') - eq(msg.epilogue, '\n') - eq(len(msg.get_payload()), 3) - # Make sure the subparts are what we expect - msg1 = msg.get_payload(0) - eq(msg1.get_content_type(), 'text/plain') - eq(msg1.get_payload(), 'Yadda yadda yadda\n') - msg2 = msg.get_payload(1) - eq(msg2.get_content_type(), 'text/plain') - eq(msg2.get_payload(), 'Yadda yadda yadda\n') - msg3 = msg.get_payload(2) - eq(msg3.get_content_type(), 'message/rfc822') - self.assertTrue(isinstance(msg3, Message)) - payload = msg3.get_payload() - unless(isinstance(payload, list)) - eq(len(payload), 1) - msg4 = payload[0] - unless(isinstance(msg4, Message)) - eq(msg4.get_payload(), 'Yadda yadda yadda\n') - - def test_parser(self): - eq = self.assertEqual - unless = self.assertTrue - msg, text = self._msgobj('msg_06.txt') - # Check some of the outer headers - eq(msg.get_content_type(), 'message/rfc822') - # Make sure the payload is a list of exactly one sub-Message, and that - # that submessage has a type of text/plain - payload = msg.get_payload() - unless(isinstance(payload, list)) - eq(len(payload), 1) - msg1 = payload[0] - self.assertTrue(isinstance(msg1, Message)) - eq(msg1.get_content_type(), 'text/plain') - self.assertTrue(isinstance(msg1.get_payload(), str)) - eq(msg1.get_payload(), '\n') - - - -# Test various other bits of the package's functionality -class TestMiscellaneous(TestEmailBase): - def test_message_from_string(self): - fp = openfile('msg_01.txt') - try: - text = fp.read() - finally: - fp.close() - msg = bongo.external.email.message_from_string(text) - s = StringIO() - # Don't wrap/continue long headers since we're trying to test - # idempotency. - g = Generator(s, maxheaderlen=0) - g.flatten(msg) - self.ndiffAssertEqual(text, s.getvalue()) - - def test_message_from_file(self): - fp = openfile('msg_01.txt') - try: - text = fp.read() - fp.seek(0) - msg = bongo.external.email.message_from_file(fp) - s = StringIO() - # Don't wrap/continue long headers since we're trying to test - # idempotency. - g = Generator(s, maxheaderlen=0) - g.flatten(msg) - self.assertEqual(text, s.getvalue()) - finally: - fp.close() - - def test_message_from_string_with_class(self): - unless = self.assertTrue - fp = openfile('msg_01.txt') - try: - text = fp.read() - finally: - fp.close() - # Create a subclass - class MyMessage(Message): - pass - - msg = bongo.external.email.message_from_string(text, MyMessage) - unless(isinstance(msg, MyMessage)) - # Try something more complicated - fp = openfile('msg_02.txt') - try: - text = fp.read() - finally: - fp.close() - msg = bongo.external.email.message_from_string(text, MyMessage) - for subpart in msg.walk(): - unless(isinstance(subpart, MyMessage)) - - def test_message_from_file_with_class(self): - unless = self.assertTrue - # Create a subclass - class MyMessage(Message): - pass - - fp = openfile('msg_01.txt') - try: - msg = bongo.external.email.message_from_file(fp, MyMessage) - finally: - fp.close() - unless(isinstance(msg, MyMessage)) - # Try something more complicated - fp = openfile('msg_02.txt') - try: - msg = bongo.external.email.message_from_file(fp, MyMessage) - finally: - fp.close() - for subpart in msg.walk(): - unless(isinstance(subpart, MyMessage)) - - def test__all__(self): - module = __import__('email') - all = module.__all__ - all.sort() - self.assertEqual(all, [ - # Old names - 'Charset', 'Encoders', 'Errors', 'Generator', - 'Header', 'Iterators', 'MIMEAudio', 'MIMEBase', - 'MIMEImage', 'MIMEMessage', 'MIMEMultipart', - 'MIMENonMultipart', 'MIMEText', 'Message', - 'Parser', 'Utils', 'base64MIME', - # new names - 'base64mime', 'charset', 'encoders', 'errors', 'generator', - 'header', 'iterators', 'message', 'message_from_file', - 'message_from_string', 'mime', 'parser', - 'quopriMIME', 'quoprimime', 'utils', - ]) - - def test_formatdate(self): - now = time.time() - self.assertEqual(Utils.parsedate(Utils.formatdate(now))[:6], - time.gmtime(now)[:6]) - - def test_formatdate_localtime(self): - now = time.time() - self.assertEqual( - Utils.parsedate(Utils.formatdate(now, localtime=True))[:6], - time.localtime(now)[:6]) - - def test_formatdate_usegmt(self): - now = time.time() - self.assertEqual( - Utils.formatdate(now, localtime=False), - time.strftime('%a, %d %b %Y %H:%M:%S -0000', time.gmtime(now))) - self.assertEqual( - Utils.formatdate(now, localtime=False, usegmt=True), - time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(now))) - - def test_parsedate_none(self): - self.assertEqual(Utils.parsedate(''), None) - - def test_parsedate_compact(self): - # The FWS after the comma is optional - self.assertEqual(Utils.parsedate('Wed,3 Apr 2002 14:58:26 +0800'), - Utils.parsedate('Wed, 3 Apr 2002 14:58:26 +0800')) - - def test_parsedate_no_dayofweek(self): - eq = self.assertEqual - eq(Utils.parsedate_tz('25 Feb 2003 13:47:26 -0800'), - (2003, 2, 25, 13, 47, 26, 0, 1, -1, -28800)) - - def test_parsedate_compact_no_dayofweek(self): - eq = self.assertEqual - eq(Utils.parsedate_tz('5 Feb 2003 13:47:26 -0800'), - (2003, 2, 5, 13, 47, 26, 0, 1, -1, -28800)) - - def test_parsedate_acceptable_to_time_functions(self): - eq = self.assertEqual - timetup = Utils.parsedate('5 Feb 2003 13:47:26 -0800') - t = int(time.mktime(timetup)) - eq(time.localtime(t)[:6], timetup[:6]) - eq(int(time.strftime('%Y', timetup)), 2003) - timetup = Utils.parsedate_tz('5 Feb 2003 13:47:26 -0800') - t = int(time.mktime(timetup[:9])) - eq(time.localtime(t)[:6], timetup[:6]) - eq(int(time.strftime('%Y', timetup[:9])), 2003) - - def test_parseaddr_empty(self): - self.assertEqual(Utils.parseaddr('<>'), ('', '')) - self.assertEqual(Utils.formataddr(Utils.parseaddr('<>')), '') - - def test_noquote_dump(self): - self.assertEqual( - Utils.formataddr(('A Silly Person', 'person@dom.ain')), - 'A Silly Person ') - - def test_escape_dump(self): - self.assertEqual( - Utils.formataddr(('A (Very) Silly Person', 'person@dom.ain')), - r'"A \(Very\) Silly Person" ') - a = r'A \(Special\) Person' - b = 'person@dom.ain' - self.assertEqual(Utils.parseaddr(Utils.formataddr((a, b))), (a, b)) - - def test_escape_backslashes(self): - self.assertEqual( - Utils.formataddr(('Arthur \Backslash\ Foobar', 'person@dom.ain')), - r'"Arthur \\Backslash\\ Foobar" ') - a = r'Arthur \Backslash\ Foobar' - b = 'person@dom.ain' - self.assertEqual(Utils.parseaddr(Utils.formataddr((a, b))), (a, b)) - - def test_name_with_dot(self): - x = 'John X. Doe ' - y = '"John X. Doe" ' - a, b = ('John X. Doe', 'jxd@example.com') - self.assertEqual(Utils.parseaddr(x), (a, b)) - self.assertEqual(Utils.parseaddr(y), (a, b)) - # formataddr() quotes the name if there's a dot in it - self.assertEqual(Utils.formataddr((a, b)), y) - - def test_quote_dump(self): - self.assertEqual( - Utils.formataddr(('A Silly; Person', 'person@dom.ain')), - r'"A Silly; Person" ') - - def test_fix_eols(self): - eq = self.assertEqual - eq(Utils.fix_eols('hello'), 'hello') - eq(Utils.fix_eols('hello\n'), 'hello\r\n') - eq(Utils.fix_eols('hello\r'), 'hello\r\n') - eq(Utils.fix_eols('hello\r\n'), 'hello\r\n') - eq(Utils.fix_eols('hello\n\r'), 'hello\r\n\r\n') - - def test_charset_richcomparisons(self): - eq = self.assertEqual - ne = self.assertNotEqual - cset1 = Charset() - cset2 = Charset() - eq(cset1, 'us-ascii') - eq(cset1, 'US-ASCII') - eq(cset1, 'Us-AsCiI') - eq('us-ascii', cset1) - eq('US-ASCII', cset1) - eq('Us-AsCiI', cset1) - ne(cset1, 'usascii') - ne(cset1, 'USASCII') - ne(cset1, 'UsAsCiI') - ne('usascii', cset1) - ne('USASCII', cset1) - ne('UsAsCiI', cset1) - eq(cset1, cset2) - eq(cset2, cset1) - - def test_getaddresses(self): - eq = self.assertEqual - eq(Utils.getaddresses(['aperson@dom.ain (Al Person)', - 'Bud Person ']), - [('Al Person', 'aperson@dom.ain'), - ('Bud Person', 'bperson@dom.ain')]) - - def test_getaddresses_nasty(self): - eq = self.assertEqual - eq(Utils.getaddresses(['foo: ;']), [('', '')]) - eq(Utils.getaddresses( - ['[]*-- =~$']), - [('', ''), ('', ''), ('', '*--')]) - eq(Utils.getaddresses( - ['foo: ;', '"Jason R. Mastaler" ']), - [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')]) - - def test_getaddresses_embedded_comment(self): - """Test proper handling of a nested comment""" - eq = self.assertEqual - addrs = Utils.getaddresses(['User ((nested comment)) ']) - eq(addrs[0][1], 'foo@bar.com') - - def test_utils_quote_unquote(self): - eq = self.assertEqual - msg = Message() - msg.add_header('content-disposition', 'attachment', - filename='foo\\wacky"name') - eq(msg.get_filename(), 'foo\\wacky"name') - - def test_get_body_encoding_with_bogus_charset(self): - charset = Charset('not a charset') - self.assertEqual(charset.get_body_encoding(), 'base64') - - def test_get_body_encoding_with_uppercase_charset(self): - eq = self.assertEqual - msg = Message() - msg['Content-Type'] = 'text/plain; charset=UTF-8' - eq(msg['content-type'], 'text/plain; charset=UTF-8') - charsets = msg.get_charsets() - eq(len(charsets), 1) - eq(charsets[0], 'utf-8') - charset = Charset(charsets[0]) - eq(charset.get_body_encoding(), 'base64') - msg.set_payload('hello world', charset=charset) - eq(msg.get_payload(), 'aGVsbG8gd29ybGQ=\n') - eq(msg.get_payload(decode=True), 'hello world') - eq(msg['content-transfer-encoding'], 'base64') - # Try another one - msg = Message() - msg['Content-Type'] = 'text/plain; charset="US-ASCII"' - charsets = msg.get_charsets() - eq(len(charsets), 1) - eq(charsets[0], 'us-ascii') - charset = Charset(charsets[0]) - eq(charset.get_body_encoding(), Encoders.encode_7or8bit) - msg.set_payload('hello world', charset=charset) - eq(msg.get_payload(), 'hello world') - eq(msg['content-transfer-encoding'], '7bit') - - def test_charsets_case_insensitive(self): - lc = Charset('us-ascii') - uc = Charset('US-ASCII') - self.assertEqual(lc.get_body_encoding(), uc.get_body_encoding()) - - def test_partial_falls_inside_message_delivery_status(self): - eq = self.ndiffAssertEqual - # The Parser interface provides chunks of data to FeedParser in 8192 - # byte gulps. SF bug #1076485 found one of those chunks inside - # message/delivery-status header block, which triggered an - # unreadline() of NeedMoreData. - msg = self._msgobj('msg_43.txt') - sfp = StringIO() - Iterators._structure(msg, sfp) - eq(sfp.getvalue(), """\ -multipart/report - text/plain - message/delivery-status - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/rfc822-headers -""") - - - -# Test the iterator/generators -class TestIterators(TestEmailBase): - def test_body_line_iterator(self): - eq = self.assertEqual - neq = self.ndiffAssertEqual - # First a simple non-multipart message - msg = self._msgobj('msg_01.txt') - it = Iterators.body_line_iterator(msg) - lines = list(it) - eq(len(lines), 6) - neq(EMPTYSTRING.join(lines), msg.get_payload()) - # Now a more complicated multipart - msg = self._msgobj('msg_02.txt') - it = Iterators.body_line_iterator(msg) - lines = list(it) - eq(len(lines), 43) - fp = openfile('msg_19.txt') - try: - neq(EMPTYSTRING.join(lines), fp.read()) - finally: - fp.close() - - def test_typed_subpart_iterator(self): - eq = self.assertEqual - msg = self._msgobj('msg_04.txt') - it = Iterators.typed_subpart_iterator(msg, 'text') - lines = [] - subparts = 0 - for subpart in it: - subparts += 1 - lines.append(subpart.get_payload()) - eq(subparts, 2) - eq(EMPTYSTRING.join(lines), """\ -a simple kind of mirror -to reflect upon our own -a simple kind of mirror -to reflect upon our own -""") - - def test_typed_subpart_iterator_default_type(self): - eq = self.assertEqual - msg = self._msgobj('msg_03.txt') - it = Iterators.typed_subpart_iterator(msg, 'text', 'plain') - lines = [] - subparts = 0 - for subpart in it: - subparts += 1 - lines.append(subpart.get_payload()) - eq(subparts, 1) - eq(EMPTYSTRING.join(lines), """\ - -Hi, - -Do you like this message? - --Me -""") - - - -class TestParsers(TestEmailBase): - def test_header_parser(self): - eq = self.assertEqual - # Parse only the headers of a complex multipart MIME document - fp = openfile('msg_02.txt') - try: - msg = HeaderParser().parse(fp) - finally: - fp.close() - eq(msg['from'], 'ppp-request@zzz.org') - eq(msg['to'], 'ppp@zzz.org') - eq(msg.get_content_type(), 'multipart/mixed') - self.assertFalse(msg.is_multipart()) - self.assertTrue(isinstance(msg.get_payload(), str)) - - def test_whitespace_continuation(self): - eq = self.assertEqual - # This message contains a line after the Subject: header that has only - # whitespace, but it is not empty! - msg = bongo.external.email.message_from_string("""\ -From: aperson@dom.ain -To: bperson@dom.ain -Subject: the next line has a space on it -\x20 -Date: Mon, 8 Apr 2002 15:09:19 -0400 -Message-ID: spam - -Here's the message body -""") - eq(msg['subject'], 'the next line has a space on it\n ') - eq(msg['message-id'], 'spam') - eq(msg.get_payload(), "Here's the message body\n") - - def test_whitespace_continuation_last_header(self): - eq = self.assertEqual - # Like the previous test, but the subject line is the last - # header. - msg = bongo.external.email.message_from_string("""\ -From: aperson@dom.ain -To: bperson@dom.ain -Date: Mon, 8 Apr 2002 15:09:19 -0400 -Message-ID: spam -Subject: the next line has a space on it -\x20 - -Here's the message body -""") - eq(msg['subject'], 'the next line has a space on it\n ') - eq(msg['message-id'], 'spam') - eq(msg.get_payload(), "Here's the message body\n") - - def test_crlf_separation(self): - eq = self.assertEqual - fp = openfile('msg_26.txt', mode='rb') - try: - msg = Parser().parse(fp) - finally: - fp.close() - eq(len(msg.get_payload()), 2) - part1 = msg.get_payload(0) - eq(part1.get_content_type(), 'text/plain') - eq(part1.get_payload(), 'Simple email with attachment.\r\n\r\n') - part2 = msg.get_payload(1) - eq(part2.get_content_type(), 'application/riscos') - - def test_multipart_digest_with_extra_mime_headers(self): - eq = self.assertEqual - neq = self.ndiffAssertEqual - fp = openfile('msg_28.txt') - try: - msg = bongo.external.email.message_from_file(fp) - finally: - fp.close() - # Structure is: - # multipart/digest - # message/rfc822 - # text/plain - # message/rfc822 - # text/plain - eq(msg.is_multipart(), 1) - eq(len(msg.get_payload()), 2) - part1 = msg.get_payload(0) - eq(part1.get_content_type(), 'message/rfc822') - eq(part1.is_multipart(), 1) - eq(len(part1.get_payload()), 1) - part1a = part1.get_payload(0) - eq(part1a.is_multipart(), 0) - eq(part1a.get_content_type(), 'text/plain') - neq(part1a.get_payload(), 'message 1\n') - # next message/rfc822 - part2 = msg.get_payload(1) - eq(part2.get_content_type(), 'message/rfc822') - eq(part2.is_multipart(), 1) - eq(len(part2.get_payload()), 1) - part2a = part2.get_payload(0) - eq(part2a.is_multipart(), 0) - eq(part2a.get_content_type(), 'text/plain') - neq(part2a.get_payload(), 'message 2\n') - - def test_three_lines(self): - # A bug report by Andrew McNamara - lines = ['From: Andrew Person From', 'From']) - eq(msg.get_payload(), 'body') - - def test_rfc2822_space_not_allowed_in_header(self): - eq = self.assertEqual - m = '>From foo@example.com 11:25:53\nFrom: bar\n!"#QUX;~: zoo\n\nbody' - msg = bongo.external.email.message_from_string(m) - eq(len(list(msg.keys())), 0) - - def test_rfc2822_one_character_header(self): - eq = self.assertEqual - m = 'A: first header\nB: second header\nCC: third header\n\nbody' - msg = bongo.external.email.message_from_string(m) - headers = list(msg.keys()) - headers.sort() - eq(headers, ['A', 'B', 'CC']) - eq(msg.get_payload(), 'body') - - - -class TestBase64(unittest.TestCase): - def test_len(self): - eq = self.assertEqual - eq(base64MIME.base64_len('hello'), - len(base64MIME.encode('hello', eol=''))) - for size in range(15): - if size == 0 : bsize = 0 - elif size <= 3 : bsize = 4 - elif size <= 6 : bsize = 8 - elif size <= 9 : bsize = 12 - elif size <= 12: bsize = 16 - else : bsize = 20 - eq(base64MIME.base64_len('x'*size), bsize) - - def test_decode(self): - eq = self.assertEqual - eq(base64MIME.decode(''), '') - eq(base64MIME.decode('aGVsbG8='), 'hello') - eq(base64MIME.decode('aGVsbG8=', 'X'), 'hello') - eq(base64MIME.decode('aGVsbG8NCndvcmxk\n', 'X'), 'helloXworld') - - def test_encode(self): - eq = self.assertEqual - eq(base64MIME.encode(''), '') - eq(base64MIME.encode('hello'), 'aGVsbG8=\n') - # Test the binary flag - eq(base64MIME.encode('hello\n'), 'aGVsbG8K\n') - eq(base64MIME.encode('hello\n', 0), 'aGVsbG8NCg==\n') - # Test the maxlinelen arg - eq(base64MIME.encode('xxxx ' * 20, maxlinelen=40), """\ -eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg -eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg -eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg -eHh4eCB4eHh4IA== -""") - # Test the eol argument - eq(base64MIME.encode('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\ -eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r -eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r -eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r -eHh4eCB4eHh4IA==\r -""") - - def test_header_encode(self): - eq = self.assertEqual - he = base64MIME.header_encode - eq(he('hello'), '=?iso-8859-1?b?aGVsbG8=?=') - eq(he('hello\nworld'), '=?iso-8859-1?b?aGVsbG8NCndvcmxk?=') - # Test the charset option - eq(he('hello', charset='iso-8859-2'), '=?iso-8859-2?b?aGVsbG8=?=') - # Test the keep_eols flag - eq(he('hello\nworld', keep_eols=True), - '=?iso-8859-1?b?aGVsbG8Kd29ybGQ=?=') - # Test the maxlinelen argument - eq(he('xxxx ' * 20, maxlinelen=40), """\ -=?iso-8859-1?b?eHh4eCB4eHh4IHh4eHggeHg=?= - =?iso-8859-1?b?eHggeHh4eCB4eHh4IHh4eHg=?= - =?iso-8859-1?b?IHh4eHggeHh4eCB4eHh4IHg=?= - =?iso-8859-1?b?eHh4IHh4eHggeHh4eCB4eHg=?= - =?iso-8859-1?b?eCB4eHh4IHh4eHggeHh4eCA=?= - =?iso-8859-1?b?eHh4eCB4eHh4IHh4eHgg?=""") - # Test the eol argument - eq(he('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\ -=?iso-8859-1?b?eHh4eCB4eHh4IHh4eHggeHg=?=\r - =?iso-8859-1?b?eHggeHh4eCB4eHh4IHh4eHg=?=\r - =?iso-8859-1?b?IHh4eHggeHh4eCB4eHh4IHg=?=\r - =?iso-8859-1?b?eHh4IHh4eHggeHh4eCB4eHg=?=\r - =?iso-8859-1?b?eCB4eHh4IHh4eHggeHh4eCA=?=\r - =?iso-8859-1?b?eHh4eCB4eHh4IHh4eHgg?=""") - - - -class TestQuopri(unittest.TestCase): - def setUp(self): - self.hlit = [chr(x) for x in range(ord('a'), ord('z')+1)] + \ - [chr(x) for x in range(ord('A'), ord('Z')+1)] + \ - [chr(x) for x in range(ord('0'), ord('9')+1)] + \ - ['!', '*', '+', '-', '/', ' '] - self.hnon = [chr(x) for x in range(256) if chr(x) not in self.hlit] - assert len(self.hlit) + len(self.hnon) == 256 - self.blit = [chr(x) for x in range(ord(' '), ord('~')+1)] + ['\t'] - self.blit.remove('=') - self.bnon = [chr(x) for x in range(256) if chr(x) not in self.blit] - assert len(self.blit) + len(self.bnon) == 256 - - def test_header_quopri_check(self): - for c in self.hlit: - self.assertFalse(quopriMIME.header_quopri_check(c)) - for c in self.hnon: - self.assertTrue(quopriMIME.header_quopri_check(c)) - - def test_body_quopri_check(self): - for c in self.blit: - self.assertFalse(quopriMIME.body_quopri_check(c)) - for c in self.bnon: - self.assertTrue(quopriMIME.body_quopri_check(c)) - - def test_header_quopri_len(self): - eq = self.assertEqual - hql = quopriMIME.header_quopri_len - enc = quopriMIME.header_encode - for s in ('hello', 'h@e@l@l@o@'): - # Empty charset and no line-endings. 7 == RFC chrome - eq(hql(s), len(enc(s, charset='', eol=''))-7) - for c in self.hlit: - eq(hql(c), 1) - for c in self.hnon: - eq(hql(c), 3) - - def test_body_quopri_len(self): - eq = self.assertEqual - bql = quopriMIME.body_quopri_len - for c in self.blit: - eq(bql(c), 1) - for c in self.bnon: - eq(bql(c), 3) - - def test_quote_unquote_idempotent(self): - for x in range(256): - c = chr(x) - self.assertEqual(quopriMIME.unquote(quopriMIME.quote(c)), c) - - def test_header_encode(self): - eq = self.assertEqual - he = quopriMIME.header_encode - eq(he('hello'), '=?iso-8859-1?q?hello?=') - eq(he('hello\nworld'), '=?iso-8859-1?q?hello=0D=0Aworld?=') - # Test the charset option - eq(he('hello', charset='iso-8859-2'), '=?iso-8859-2?q?hello?=') - # Test the keep_eols flag - eq(he('hello\nworld', keep_eols=True), '=?iso-8859-1?q?hello=0Aworld?=') - # Test a non-ASCII character - eq(he('hello\xc7there'), '=?iso-8859-1?q?hello=C7there?=') - # Test the maxlinelen argument - eq(he('xxxx ' * 20, maxlinelen=40), """\ -=?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xx?= - =?iso-8859-1?q?xx_xxxx_xxxx_xxxx_xxxx?= - =?iso-8859-1?q?_xxxx_xxxx_xxxx_xxxx_x?= - =?iso-8859-1?q?xxx_xxxx_xxxx_xxxx_xxx?= - =?iso-8859-1?q?x_xxxx_xxxx_?=""") - # Test the eol argument - eq(he('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\ -=?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xx?=\r - =?iso-8859-1?q?xx_xxxx_xxxx_xxxx_xxxx?=\r - =?iso-8859-1?q?_xxxx_xxxx_xxxx_xxxx_x?=\r - =?iso-8859-1?q?xxx_xxxx_xxxx_xxxx_xxx?=\r - =?iso-8859-1?q?x_xxxx_xxxx_?=""") - - def test_decode(self): - eq = self.assertEqual - eq(quopriMIME.decode(''), '') - eq(quopriMIME.decode('hello'), 'hello') - eq(quopriMIME.decode('hello', 'X'), 'hello') - eq(quopriMIME.decode('hello\nworld', 'X'), 'helloXworld') - - def test_encode(self): - eq = self.assertEqual - eq(quopriMIME.encode(''), '') - eq(quopriMIME.encode('hello'), 'hello') - # Test the binary flag - eq(quopriMIME.encode('hello\r\nworld'), 'hello\nworld') - eq(quopriMIME.encode('hello\r\nworld', 0), 'hello\nworld') - # Test the maxlinelen arg - eq(quopriMIME.encode('xxxx ' * 20, maxlinelen=40), """\ -xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx= - xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx= -x xxxx xxxx xxxx xxxx=20""") - # Test the eol argument - eq(quopriMIME.encode('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\ -xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=\r - xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=\r -x xxxx xxxx xxxx xxxx=20""") - eq(quopriMIME.encode("""\ -one line - -two line"""), """\ -one line - -two line""") - - - -# Test the Charset class -class TestCharset(unittest.TestCase): - def tearDown(self): - from bongo.external.email import charset as CharsetModule - try: - del CharsetModule.CHARSETS['fake'] - except KeyError: - pass - - def test_idempotent(self): - eq = self.assertEqual - # Make sure us-ascii = no Unicode conversion - c = Charset('us-ascii') - s = 'Hello World!' - sp = c.to_splittable(s) - eq(s, c.from_splittable(sp)) - # test 8-bit idempotency with us-ascii - s = '\xa4\xa2\xa4\xa4\xa4\xa6\xa4\xa8\xa4\xaa' - sp = c.to_splittable(s) - eq(s, c.from_splittable(sp)) - - def test_body_encode(self): - eq = self.assertEqual - # Try a charset with QP body encoding - c = Charset('iso-8859-1') - eq('hello w=F6rld', c.body_encode('hello w\xf6rld')) - # Try a charset with Base64 body encoding - c = Charset('utf-8') - eq('aGVsbG8gd29ybGQ=\n', c.body_encode('hello world')) - # Try a charset with None body encoding - c = Charset('us-ascii') - eq('hello world', c.body_encode('hello world')) - # Try the convert argument, where input codec <> output codec - c = Charset('euc-jp') - # With apologies to Tokio Kikuchi ;) - try: - eq('\x1b$B5FCO;~IW\x1b(B', - c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7')) - eq('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7', - c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7', False)) - except LookupError: - # We probably don't have the Japanese codecs installed - pass - # Testing SF bug #625509, which we have to fake, since there are no - # built-in encodings where the header encoding is QP but the body - # encoding is not. - from bongo.external.email import charset as CharsetModule - CharsetModule.add_charset('fake', CharsetModule.QP, None) - c = Charset('fake') - eq('hello w\xf6rld', c.body_encode('hello w\xf6rld')) - - def test_unicode_charset_name(self): - charset = Charset('us-ascii') - self.assertEqual(str(charset), 'us-ascii') - self.assertRaises(Errors.CharsetError, Charset, 'asc\xffii') - - - -# Test multilingual MIME headers. -class TestHeader(TestEmailBase): - def test_simple(self): - eq = self.ndiffAssertEqual - h = Header('Hello World!') - eq(h.encode(), 'Hello World!') - h.append(' Goodbye World!') - eq(h.encode(), 'Hello World! Goodbye World!') - - def test_simple_surprise(self): - eq = self.ndiffAssertEqual - h = Header('Hello World!') - eq(h.encode(), 'Hello World!') - h.append('Goodbye World!') - eq(h.encode(), 'Hello World! Goodbye World!') - - def test_header_needs_no_decoding(self): - h = 'no decoding needed' - self.assertEqual(decode_header(h), [(h, None)]) - - def test_long(self): - h = Header("I am the very model of a modern Major-General; I've information vegetable, animal, and mineral; I know the kings of England, and I quote the fights historical from Marathon to Waterloo, in order categorical; I'm very well acquainted, too, with matters mathematical; I understand equations, both the simple and quadratical; about binomial theorem I'm teeming with a lot o' news, with many cheerful facts about the square of the hypotenuse.", - maxlinelen=76) - for l in h.encode(splitchars=' ').split('\n '): - self.assertTrue(len(l) <= 76) - - def test_multilingual(self): - eq = self.ndiffAssertEqual - g = Charset("iso-8859-1") - cz = Charset("iso-8859-2") - utf8 = Charset("utf-8") - g_head = "Die Mieter treten hier ein werden mit einem Foerderband komfortabel den Korridor entlang, an s\xfcdl\xfcndischen Wandgem\xe4lden vorbei, gegen die rotierenden Klingen bef\xf6rdert. " - cz_head = "Finan\xe8ni metropole se hroutily pod tlakem jejich d\xf9vtipu.. " - utf8_head = "\u6b63\u78ba\u306b\u8a00\u3046\u3068\u7ffb\u8a33\u306f\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u4e00\u90e8\u306f\u30c9\u30a4\u30c4\u8a9e\u3067\u3059\u304c\u3001\u3042\u3068\u306f\u3067\u305f\u3089\u3081\u3067\u3059\u3002\u5b9f\u969b\u306b\u306f\u300cWenn ist das Nunstuck git und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt gersput.\u300d\u3068\u8a00\u3063\u3066\u3044\u307e\u3059\u3002".encode("utf-8") - h = Header(g_head, g) - h.append(cz_head, cz) - h.append(utf8_head, utf8) - enc = h.encode() - eq(enc, """\ -=?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerderband_ko?= - =?iso-8859-1?q?mfortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndischen_Wan?= - =?iso-8859-1?q?dgem=E4lden_vorbei=2C_gegen_die_rotierenden_Klingen_bef=F6?= - =?iso-8859-1?q?rdert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se_hroutily?= - =?iso-8859-2?q?_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= =?utf-8?b?5q2j56K6?= - =?utf-8?b?44Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb44KT44CC?= - =?utf-8?b?5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go44Gv44Gn?= - =?utf-8?b?44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBpc3QgZGFz?= - =?utf-8?q?_Nunstuck_git_und_Slotermeyer=3F_Ja!_Beiherhund_das_Oder_die_Fl?= - =?utf-8?b?aXBwZXJ3YWxkdCBnZXJzcHV0LuOAjeOBqOiogOOBo+OBpuOBhOOBvuOBmQ==?= - =?utf-8?b?44CC?=""") - eq(decode_header(enc), - [(g_head, "iso-8859-1"), (cz_head, "iso-8859-2"), - (utf8_head, "utf-8")]) - ustr = str(h) - eq(ustr.encode('utf-8'), - 'Die Mieter treten hier ein werden mit einem Foerderband ' - 'komfortabel den Korridor entlang, an s\xc3\xbcdl\xc3\xbcndischen ' - 'Wandgem\xc3\xa4lden vorbei, gegen die rotierenden Klingen ' - 'bef\xc3\xb6rdert. Finan\xc4\x8dni metropole se hroutily pod ' - 'tlakem jejich d\xc5\xafvtipu.. \xe6\xad\xa3\xe7\xa2\xba\xe3\x81' - '\xab\xe8\xa8\x80\xe3\x81\x86\xe3\x81\xa8\xe7\xbf\xbb\xe8\xa8\xb3' - '\xe3\x81\xaf\xe3\x81\x95\xe3\x82\x8c\xe3\x81\xa6\xe3\x81\x84\xe3' - '\x81\xbe\xe3\x81\x9b\xe3\x82\x93\xe3\x80\x82\xe4\xb8\x80\xe9\x83' - '\xa8\xe3\x81\xaf\xe3\x83\x89\xe3\x82\xa4\xe3\x83\x84\xe8\xaa\x9e' - '\xe3\x81\xa7\xe3\x81\x99\xe3\x81\x8c\xe3\x80\x81\xe3\x81\x82\xe3' - '\x81\xa8\xe3\x81\xaf\xe3\x81\xa7\xe3\x81\x9f\xe3\x82\x89\xe3\x82' - '\x81\xe3\x81\xa7\xe3\x81\x99\xe3\x80\x82\xe5\xae\x9f\xe9\x9a\x9b' - '\xe3\x81\xab\xe3\x81\xaf\xe3\x80\x8cWenn ist das Nunstuck git ' - 'und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt ' - 'gersput.\xe3\x80\x8d\xe3\x81\xa8\xe8\xa8\x80\xe3\x81\xa3\xe3\x81' - '\xa6\xe3\x81\x84\xe3\x81\xbe\xe3\x81\x99\xe3\x80\x82') - # Test make_header() - newh = make_header(decode_header(enc)) - eq(newh, enc) - - def test_header_ctor_default_args(self): - eq = self.ndiffAssertEqual - h = Header() - eq(h, '') - h.append('foo', Charset('iso-8859-1')) - eq(h, '=?iso-8859-1?q?foo?=') - - def test_explicit_maxlinelen(self): - eq = self.ndiffAssertEqual - hstr = 'A very long line that must get split to something other than at the 76th character boundary to test the non-default behavior' - h = Header(hstr) - eq(h.encode(), '''\ -A very long line that must get split to something other than at the 76th - character boundary to test the non-default behavior''') - h = Header(hstr, header_name='Subject') - eq(h.encode(), '''\ -A very long line that must get split to something other than at the - 76th character boundary to test the non-default behavior''') - h = Header(hstr, maxlinelen=1024, header_name='Subject') - eq(h.encode(), hstr) - - def test_us_ascii_header(self): - eq = self.assertEqual - s = 'hello' - x = decode_header(s) - eq(x, [('hello', None)]) - h = make_header(x) - eq(s, h.encode()) - - def test_string_charset(self): - eq = self.assertEqual - h = Header() - h.append('hello', 'iso-8859-1') - eq(h, '=?iso-8859-1?q?hello?=') - -## def test_unicode_error(self): -## raises = self.assertRaises -## raises(UnicodeError, Header, u'[P\xf6stal]', 'us-ascii') -## raises(UnicodeError, Header, '[P\xf6stal]', 'us-ascii') -## h = Header() -## raises(UnicodeError, h.append, u'[P\xf6stal]', 'us-ascii') -## raises(UnicodeError, h.append, '[P\xf6stal]', 'us-ascii') -## raises(UnicodeError, Header, u'\u83ca\u5730\u6642\u592b', 'iso-8859-1') - - def test_utf8_shortest(self): - eq = self.assertEqual - h = Header('p\xf6stal', 'utf-8') - eq(h.encode(), '=?utf-8?q?p=C3=B6stal?=') - h = Header('\u83ca\u5730\u6642\u592b', 'utf-8') - eq(h.encode(), '=?utf-8?b?6I+K5Zyw5pmC5aSr?=') - - def test_bad_8bit_header(self): - raises = self.assertRaises - eq = self.assertEqual - x = 'Ynwp4dUEbay Auction Semiar- No Charge \x96 Earn Big' - raises(UnicodeError, Header, x) - h = Header() - raises(UnicodeError, h.append, x) - eq(str(Header(x, errors='replace')), x) - h.append(x, errors='replace') - eq(str(h), x) - - def test_encoded_adjacent_nonencoded(self): - eq = self.assertEqual - h = Header() - h.append('hello', 'iso-8859-1') - h.append('world') - s = h.encode() - eq(s, '=?iso-8859-1?q?hello?= world') - h = make_header(decode_header(s)) - eq(h.encode(), s) - - def test_whitespace_eater(self): - eq = self.assertEqual - s = 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztk=?= =?koi8-r?q?=CA?= zz.' - parts = decode_header(s) - eq(parts, [('Subject:', None), ('\xf0\xd2\xcf\xd7\xc5\xd2\xcb\xc1 \xce\xc1 \xc6\xc9\xce\xc1\xcc\xd8\xce\xd9\xca', 'koi8-r'), ('zz.', None)]) - hdr = make_header(parts) - eq(hdr.encode(), - 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztnK?= zz.') - - def test_broken_base64_header(self): - raises = self.assertRaises - s = 'Subject: =?EUC-KR?B?CSixpLDtKSC/7Liuvsax4iC6uLmwMcijIKHaILzSwd/H0SC8+LCjwLsgv7W/+Mj3IQ?=' - raises(Errors.HeaderParseError, decode_header, s) - - - -# Test RFC 2231 header parameters (en/de)coding -class TestRFC2231(TestEmailBase): - def test_get_param(self): - eq = self.ndiffAssertEqual - msg = self._msgobj('msg_29.txt') - eq(msg.get_param('title'), - ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!')) - eq(msg.get_param('title', unquote=False), - ('us-ascii', 'en', '"This is even more ***fun*** isn\'t it!"')) - - def test_set_param(self): - eq = self.ndiffAssertEqual - msg = Message() - msg.set_param('title', 'This is even more ***fun*** isn\'t it!', - charset='us-ascii') - eq(msg.get_param('title'), - ('us-ascii', '', 'This is even more ***fun*** isn\'t it!')) - msg.set_param('title', 'This is even more ***fun*** isn\'t it!', - charset='us-ascii', language='en') - eq(msg.get_param('title'), - ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!')) - msg = self._msgobj('msg_01.txt') - - msg.set_param('title', 'This is even more ***fun*** isn\'t it!', - charset='us-ascii', language='en') - - correct = """\ -Return-Path: -Delivered-To: bbb@zzz.org -Received: by mail.zzz.org (Postfix, from userid 889) -\tid 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT) -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit -Message-ID: <15090.61304.110929.45684@aaa.zzz.org> -From: bbb@ddd.com (John X. Doe) -To: bbb@zzz.org -Subject: This is a test message -Date: Fri, 4 May 2001 14:05:44 -0400 -Content-Type: text/plain; charset=us-ascii; -\ttitle*="us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21" - - -Hi, - -Do you like this message? - --Me -""" - eq(msg.as_string(), correct) - - def test_del_param(self): - eq = self.ndiffAssertEqual - msg = self._msgobj('msg_01.txt') - msg.set_param('foo', 'bar', charset='us-ascii', language='en') - msg.set_param('title', 'This is even more ***fun*** isn\'t it!', - charset='us-ascii', language='en') - msg.del_param('foo', header='Content-Type') - eq(msg.as_string(), """\ -Return-Path: -Delivered-To: bbb@zzz.org -Received: by mail.zzz.org (Postfix, from userid 889) -\tid 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT) -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit -Message-ID: <15090.61304.110929.45684@aaa.zzz.org> -From: bbb@ddd.com (John X. Doe) -To: bbb@zzz.org -Subject: This is a test message -Date: Fri, 4 May 2001 14:05:44 -0400 -Content-Type: text/plain; charset="us-ascii"; -\ttitle*="us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21" - - -Hi, - -Do you like this message? - --Me -""") - - def test_rfc2231_get_content_charset(self): - eq = self.assertEqual - msg = self._msgobj('msg_32.txt') - eq(msg.get_content_charset(), 'us-ascii') - - def test_rfc2231_no_language_or_charset(self): - m = '''\ -Content-Transfer-Encoding: 8bit -Content-Disposition: inline; filename="file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm" -Content-Type: text/html; NAME*0=file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEM; NAME*1=P_nsmail.htm - -''' - msg = bongo.external.email.message_from_string(m) - param = msg.get_param('NAME') - self.assertFalse(isinstance(param, tuple)) - self.assertEqual( - param, - 'file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm') - - def test_rfc2231_no_language_or_charset_in_filename(self): - m = '''\ -Content-Disposition: inline; -\tfilename*0*="''This%20is%20even%20more%20"; -\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20"; -\tfilename*2="is it not.pdf" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual(msg.get_filename(), - 'This is even more ***fun*** is it not.pdf') - - def test_rfc2231_no_language_or_charset_in_filename_encoded(self): - m = '''\ -Content-Disposition: inline; -\tfilename*0*="''This%20is%20even%20more%20"; -\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20"; -\tfilename*2="is it not.pdf" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual(msg.get_filename(), - 'This is even more ***fun*** is it not.pdf') - - def test_rfc2231_partly_encoded(self): - m = '''\ -Content-Disposition: inline; -\tfilename*0="''This%20is%20even%20more%20"; -\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20"; -\tfilename*2="is it not.pdf" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual( - msg.get_filename(), - 'This%20is%20even%20more%20***fun*** is it not.pdf') - - def test_rfc2231_partly_nonencoded(self): - m = '''\ -Content-Disposition: inline; -\tfilename*0="This%20is%20even%20more%20"; -\tfilename*1="%2A%2A%2Afun%2A%2A%2A%20"; -\tfilename*2="is it not.pdf" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual( - msg.get_filename(), - 'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20is it not.pdf') - - def test_rfc2231_no_language_or_charset_in_boundary(self): - m = '''\ -Content-Type: multipart/alternative; -\tboundary*0*="''This%20is%20even%20more%20"; -\tboundary*1*="%2A%2A%2Afun%2A%2A%2A%20"; -\tboundary*2="is it not.pdf" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual(msg.get_boundary(), - 'This is even more ***fun*** is it not.pdf') - - def test_rfc2231_no_language_or_charset_in_charset(self): - # This is a nonsensical charset value, but tests the code anyway - m = '''\ -Content-Type: text/plain; -\tcharset*0*="This%20is%20even%20more%20"; -\tcharset*1*="%2A%2A%2Afun%2A%2A%2A%20"; -\tcharset*2="is it not.pdf" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual(msg.get_content_charset(), - 'this is even more ***fun*** is it not.pdf') - - def test_rfc2231_bad_encoding_in_filename(self): - m = '''\ -Content-Disposition: inline; -\tfilename*0*="bogus'xx'This%20is%20even%20more%20"; -\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20"; -\tfilename*2="is it not.pdf" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual(msg.get_filename(), - 'This is even more ***fun*** is it not.pdf') - - def test_rfc2231_bad_encoding_in_charset(self): - m = """\ -Content-Type: text/plain; charset*=bogus''utf-8%E2%80%9D - -""" - msg = bongo.external.email.message_from_string(m) - # This should return None because non-ascii characters in the charset - # are not allowed. - self.assertEqual(msg.get_content_charset(), None) - - def test_rfc2231_bad_character_in_charset(self): - m = """\ -Content-Type: text/plain; charset*=ascii''utf-8%E2%80%9D - -""" - msg = bongo.external.email.message_from_string(m) - # This should return None because non-ascii characters in the charset - # are not allowed. - self.assertEqual(msg.get_content_charset(), None) - - def test_rfc2231_bad_character_in_filename(self): - m = '''\ -Content-Disposition: inline; -\tfilename*0*="ascii'xx'This%20is%20even%20more%20"; -\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20"; -\tfilename*2*="is it not.pdf%E2" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual(msg.get_filename(), - 'This is even more ***fun*** is it not.pdf\ufffd') - - def test_rfc2231_unknown_encoding(self): - m = """\ -Content-Transfer-Encoding: 8bit -Content-Disposition: inline; filename*=X-UNKNOWN''myfile.txt - -""" - msg = bongo.external.email.message_from_string(m) - self.assertEqual(msg.get_filename(), 'myfile.txt') - - def test_rfc2231_single_tick_in_filename_extended(self): - eq = self.assertEqual - m = """\ -Content-Type: application/x-foo; -\tname*0*=\"Frank's\"; name*1*=\" Document\" - -""" - msg = bongo.external.email.message_from_string(m) - charset, language, s = msg.get_param('name') - eq(charset, None) - eq(language, None) - eq(s, "Frank's Document") - - def test_rfc2231_single_tick_in_filename(self): - m = """\ -Content-Type: application/x-foo; name*0=\"Frank's\"; name*1=\" Document\" - -""" - msg = bongo.external.email.message_from_string(m) - param = msg.get_param('name') - self.assertFalse(isinstance(param, tuple)) - self.assertEqual(param, "Frank's Document") - - def test_rfc2231_tick_attack_extended(self): - eq = self.assertEqual - m = """\ -Content-Type: application/x-foo; -\tname*0*=\"us-ascii'en-us'Frank's\"; name*1*=\" Document\" - -""" - msg = bongo.external.email.message_from_string(m) - charset, language, s = msg.get_param('name') - eq(charset, 'us-ascii') - eq(language, 'en-us') - eq(s, "Frank's Document") - - def test_rfc2231_tick_attack(self): - m = """\ -Content-Type: application/x-foo; -\tname*0=\"us-ascii'en-us'Frank's\"; name*1=\" Document\" - -""" - msg = bongo.external.email.message_from_string(m) - param = msg.get_param('name') - self.assertFalse(isinstance(param, tuple)) - self.assertEqual(param, "us-ascii'en-us'Frank's Document") - - def test_rfc2231_no_extended_values(self): - eq = self.assertEqual - m = """\ -Content-Type: application/x-foo; name=\"Frank's Document\" - -""" - msg = bongo.external.email.message_from_string(m) - eq(msg.get_param('name'), "Frank's Document") - - def test_rfc2231_encoded_then_unencoded_segments(self): - eq = self.assertEqual - m = """\ -Content-Type: application/x-foo; -\tname*0*=\"us-ascii'en-us'My\"; -\tname*1=\" Document\"; -\tname*2*=\" For You\" - -""" - msg = bongo.external.email.message_from_string(m) - charset, language, s = msg.get_param('name') - eq(charset, 'us-ascii') - eq(language, 'en-us') - eq(s, 'My Document For You') - - def test_rfc2231_unencoded_then_encoded_segments(self): - eq = self.assertEqual - m = """\ -Content-Type: application/x-foo; -\tname*0=\"us-ascii'en-us'My\"; -\tname*1*=\" Document\"; -\tname*2*=\" For You\" - -""" - msg = bongo.external.email.message_from_string(m) - charset, language, s = msg.get_param('name') - eq(charset, 'us-ascii') - eq(language, 'en-us') - eq(s, 'My Document For You') - - - -def _testclasses(): - mod = sys.modules[__name__] - return [getattr(mod, name) for name in dir(mod) if name.startswith('Test')] - - -def suite(): - suite = unittest.TestSuite() - for testclass in _testclasses(): - suite.addTest(unittest.makeSuite(testclass)) - return suite - - -def test_main(): - for testclass in _testclasses(): - run_unittest(testclass) - - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/src/libs/python/bongo/external/email/test/test_email_codecs.py b/src/libs/python/bongo/external/email/test/test_email_codecs.py deleted file mode 100644 index 718df48..0000000 --- a/src/libs/python/bongo/external/email/test/test_email_codecs.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright (C) 2002-2006 Python Software Foundation -# Contact: email-sig@python.org -# email package unit tests for (optional) Asian codecs - -import unittest -from test.test_support import TestSkipped, run_unittest - -from bongo.external.email.test.test_email import TestEmailBase -from bongo.external.email.Charset import Charset -from bongo.external.email.Header import Header, decode_header -from bongo.external.email.Message import Message - -# We're compatible with Python 2.3, but it doesn't have the built-in Asian -# codecs, so we have to skip all these tests. -try: - str('foo', 'euc-jp') -except LookupError: - raise TestSkipped - - - -class TestEmailAsianCodecs(TestEmailBase): - def test_japanese_codecs(self): - eq = self.ndiffAssertEqual - j = Charset("euc-jp") - g = Charset("iso-8859-1") - h = Header("Hello World!") - jhello = '\xa5\xcf\xa5\xed\xa1\xbc\xa5\xef\xa1\xbc\xa5\xeb\xa5\xc9\xa1\xaa' - ghello = 'Gr\xfc\xdf Gott!' - h.append(jhello, j) - h.append(ghello, g) - # BAW: This used to -- and maybe should -- fold the two iso-8859-1 - # chunks into a single encoded word. However it doesn't violate the - # standard to have them as two encoded chunks and maybe it's - # reasonable for each .append() call to result in a separate - # encoded word. - eq(h.encode(), """\ -Hello World! =?iso-2022-jp?b?GyRCJU8lbSE8JW8hPCVrJUkhKhsoQg==?= - =?iso-8859-1?q?Gr=FC=DF?= =?iso-8859-1?q?_Gott!?=""") - eq(decode_header(h.encode()), - [('Hello World!', None), - ('\x1b$B%O%m!<%o!<%k%I!*\x1b(B', 'iso-2022-jp'), - ('Gr\xfc\xdf Gott!', 'iso-8859-1')]) - long = 'test-ja \xa4\xd8\xc5\xea\xb9\xc6\xa4\xb5\xa4\xec\xa4\xbf\xa5\xe1\xa1\xbc\xa5\xeb\xa4\xcf\xbb\xca\xb2\xf1\xbc\xd4\xa4\xce\xbe\xb5\xc7\xa7\xa4\xf2\xc2\xd4\xa4\xc3\xa4\xc6\xa4\xa4\xa4\xde\xa4\xb9' - h = Header(int, j, header_name="Subject") - # test a very long header - enc = h.encode() - # TK: splitting point may differ by codec design and/or Header encoding - eq(enc , """\ -=?iso-2022-jp?b?dGVzdC1qYSAbJEIkWEVqOUYkNSRsJD8lYSE8JWskTztKGyhC?= - =?iso-2022-jp?b?GyRCMnE8VCROPjVHJyRyQlQkQyRGJCQkXiQ5GyhC?=""") - # TK: full decode comparison - eq(h.__unicode__().encode('euc-jp'), int) - - def test_payload_encoding(self): - jhello = '\xa5\xcf\xa5\xed\xa1\xbc\xa5\xef\xa1\xbc\xa5\xeb\xa5\xc9\xa1\xaa' - jcode = 'euc-jp' - msg = Message() - msg.set_payload(jhello, jcode) - ustr = str(msg.get_payload(), msg.get_content_charset()) - self.assertEqual(jhello, ustr.encode(jcode)) - - - -def suite(): - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(TestEmailAsianCodecs)) - return suite - - -def test_main(): - run_unittest(TestEmailAsianCodecs) - - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/src/libs/python/bongo/external/email/test/test_email_codecs_renamed.py b/src/libs/python/bongo/external/email/test/test_email_codecs_renamed.py deleted file mode 100644 index d033437..0000000 --- a/src/libs/python/bongo/external/email/test/test_email_codecs_renamed.py +++ /dev/null @@ -1,77 +0,0 @@ -# Copyright (C) 2002-2006 Python Software Foundation -# Contact: email-sig@python.org -# email package unit tests for (optional) Asian codecs - -import unittest -from test.test_support import TestSkipped, run_unittest - -from bongo.external.email.test.test_email import TestEmailBase -from bongo.external.email.charset import Charset -from bongo.external.email.header import Header, decode_header -from bongo.external.email.message import Message - -# We're compatible with Python 2.3, but it doesn't have the built-in Asian -# codecs, so we have to skip all these tests. -try: - str('foo', 'euc-jp') -except LookupError: - raise TestSkipped - - - -class TestEmailAsianCodecs(TestEmailBase): - def test_japanese_codecs(self): - eq = self.ndiffAssertEqual - j = Charset("euc-jp") - g = Charset("iso-8859-1") - h = Header("Hello World!") - jhello = '\xa5\xcf\xa5\xed\xa1\xbc\xa5\xef\xa1\xbc\xa5\xeb\xa5\xc9\xa1\xaa' - ghello = 'Gr\xfc\xdf Gott!' - h.append(jhello, j) - h.append(ghello, g) - # BAW: This used to -- and maybe should -- fold the two iso-8859-1 - # chunks into a single encoded word. However it doesn't violate the - # standard to have them as two encoded chunks and maybe it's - # reasonable for each .append() call to result in a separate - # encoded word. - eq(h.encode(), """\ -Hello World! =?iso-2022-jp?b?GyRCJU8lbSE8JW8hPCVrJUkhKhsoQg==?= - =?iso-8859-1?q?Gr=FC=DF?= =?iso-8859-1?q?_Gott!?=""") - eq(decode_header(h.encode()), - [('Hello World!', None), - ('\x1b$B%O%m!<%o!<%k%I!*\x1b(B', 'iso-2022-jp'), - ('Gr\xfc\xdf Gott!', 'iso-8859-1')]) - long = 'test-ja \xa4\xd8\xc5\xea\xb9\xc6\xa4\xb5\xa4\xec\xa4\xbf\xa5\xe1\xa1\xbc\xa5\xeb\xa4\xcf\xbb\xca\xb2\xf1\xbc\xd4\xa4\xce\xbe\xb5\xc7\xa7\xa4\xf2\xc2\xd4\xa4\xc3\xa4\xc6\xa4\xa4\xa4\xde\xa4\xb9' - h = Header(int, j, header_name="Subject") - # test a very long header - enc = h.encode() - # TK: splitting point may differ by codec design and/or Header encoding - eq(enc , """\ -=?iso-2022-jp?b?dGVzdC1qYSAbJEIkWEVqOUYkNSRsJD8lYSE8JWskTztKGyhC?= - =?iso-2022-jp?b?GyRCMnE8VCROPjVHJyRyQlQkQyRGJCQkXiQ5GyhC?=""") - # TK: full decode comparison - eq(h.__unicode__().encode('euc-jp'), int) - - def test_payload_encoding(self): - jhello = '\xa5\xcf\xa5\xed\xa1\xbc\xa5\xef\xa1\xbc\xa5\xeb\xa5\xc9\xa1\xaa' - jcode = 'euc-jp' - msg = Message() - msg.set_payload(jhello, jcode) - ustr = str(msg.get_payload(), msg.get_content_charset()) - self.assertEqual(jhello, ustr.encode(jcode)) - - - -def suite(): - suite = unittest.TestSuite() - suite.addTest(unittest.makeSuite(TestEmailAsianCodecs)) - return suite - - -def test_main(): - run_unittest(TestEmailAsianCodecs) - - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/src/libs/python/bongo/external/email/test/test_email_renamed.py b/src/libs/python/bongo/external/email/test/test_email_renamed.py deleted file mode 100644 index 27a6b27..0000000 --- a/src/libs/python/bongo/external/email/test/test_email_renamed.py +++ /dev/null @@ -1,3255 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Contact: email-sig@python.org -# email package unit tests - -import os -import sys -import time -import base64 -import difflib -import unittest -import warnings -from io import StringIO - -import email - -from bongo.external.email.charset import Charset -from bongo.external.email.header import Header, decode_header, make_header -from bongo.external.email.parser import Parser, HeaderParser -from bongo.external.email.generator import Generator, DecodedGenerator -from bongo.external.email.message import Message -from bongo.external.email.mime.application import MIMEApplication -from bongo.external.email.mime.audio import MIMEAudio -from bongo.external.email.mime.text import MIMEText -from bongo.external.email.mime.image import MIMEImage -from bongo.external.email.mime.base import MIMEBase -from bongo.external.email.mime.message import MIMEMessage -from bongo.external.email.mime.multipart import MIMEMultipart -from bongo.external.email import utils -from bongo.external.email import errors -from bongo.external.email import encoders -from bongo.external.email import iterators -from bongo.external.email import base64mime -from bongo.external.email import quoprimime - -from test.test_support import findfile, run_unittest -from bongo.external.email.test import __file__ as landmark - - -NL = '\n' -EMPTYSTRING = '' -SPACE = ' ' - - - -def openfile(filename, mode='r'): - path = os.path.join(os.path.dirname(landmark), 'data', filename) - return open(path, mode) - - - -# Base test class -class TestEmailBase(unittest.TestCase): - def ndiffAssertEqual(self, first, second): - """Like failUnlessEqual except use ndiff for readable output.""" - if first != second: - sfirst = str(first) - ssecond = str(second) - diff = difflib.ndiff(sfirst.splitlines(), ssecond.splitlines()) - fp = StringIO() - print(NL, NL.join(diff), file=fp) - raise self.failureException(fp.getvalue()) - - def _msgobj(self, filename): - fp = openfile(findfile(filename)) - try: - msg = bongo.external.email.message_from_file(fp) - finally: - fp.close() - return msg - - - -# Test various aspects of the Message class's API -class TestMessageAPI(TestEmailBase): - def test_get_all(self): - eq = self.assertEqual - msg = self._msgobj('msg_20.txt') - eq(msg.get_all('cc'), ['ccc@zzz.org', 'ddd@zzz.org', 'eee@zzz.org']) - eq(msg.get_all('xx', 'n/a'), 'n/a') - - def test_getset_charset(self): - eq = self.assertEqual - msg = Message() - eq(msg.get_charset(), None) - charset = Charset('iso-8859-1') - msg.set_charset(charset) - eq(msg['mime-version'], '1.0') - eq(msg.get_content_type(), 'text/plain') - eq(msg['content-type'], 'text/plain; charset="iso-8859-1"') - eq(msg.get_param('charset'), 'iso-8859-1') - eq(msg['content-transfer-encoding'], 'quoted-printable') - eq(msg.get_charset().input_charset, 'iso-8859-1') - # Remove the charset - msg.set_charset(None) - eq(msg.get_charset(), None) - eq(msg['content-type'], 'text/plain') - # Try adding a charset when there's already MIME headers present - msg = Message() - msg['MIME-Version'] = '2.0' - msg['Content-Type'] = 'text/x-weird' - msg['Content-Transfer-Encoding'] = 'quinted-puntable' - msg.set_charset(charset) - eq(msg['mime-version'], '2.0') - eq(msg['content-type'], 'text/x-weird; charset="iso-8859-1"') - eq(msg['content-transfer-encoding'], 'quinted-puntable') - - def test_set_charset_from_string(self): - eq = self.assertEqual - msg = Message() - msg.set_charset('us-ascii') - eq(msg.get_charset().input_charset, 'us-ascii') - eq(msg['content-type'], 'text/plain; charset="us-ascii"') - - def test_set_payload_with_charset(self): - msg = Message() - charset = Charset('iso-8859-1') - msg.set_payload('This is a string payload', charset) - self.assertEqual(msg.get_charset().input_charset, 'iso-8859-1') - - def test_get_charsets(self): - eq = self.assertEqual - - msg = self._msgobj('msg_08.txt') - charsets = msg.get_charsets() - eq(charsets, [None, 'us-ascii', 'iso-8859-1', 'iso-8859-2', 'koi8-r']) - - msg = self._msgobj('msg_09.txt') - charsets = msg.get_charsets('dingbat') - eq(charsets, ['dingbat', 'us-ascii', 'iso-8859-1', 'dingbat', - 'koi8-r']) - - msg = self._msgobj('msg_12.txt') - charsets = msg.get_charsets() - eq(charsets, [None, 'us-ascii', 'iso-8859-1', None, 'iso-8859-2', - 'iso-8859-3', 'us-ascii', 'koi8-r']) - - def test_get_filename(self): - eq = self.assertEqual - - msg = self._msgobj('msg_04.txt') - filenames = [p.get_filename() for p in msg.get_payload()] - eq(filenames, ['msg.txt', 'msg.txt']) - - msg = self._msgobj('msg_07.txt') - subpart = msg.get_payload(1) - eq(subpart.get_filename(), 'dingusfish.gif') - - def test_get_filename_with_name_parameter(self): - eq = self.assertEqual - - msg = self._msgobj('msg_44.txt') - filenames = [p.get_filename() for p in msg.get_payload()] - eq(filenames, ['msg.txt', 'msg.txt']) - - def test_get_boundary(self): - eq = self.assertEqual - msg = self._msgobj('msg_07.txt') - # No quotes! - eq(msg.get_boundary(), 'BOUNDARY') - - def test_set_boundary(self): - eq = self.assertEqual - # This one has no existing boundary parameter, but the Content-Type: - # header appears fifth. - msg = self._msgobj('msg_01.txt') - msg.set_boundary('BOUNDARY') - header, value = list(msg.items())[4] - eq(header.lower(), 'content-type') - eq(value, 'text/plain; charset="us-ascii"; boundary="BOUNDARY"') - # This one has a Content-Type: header, with a boundary, stuck in the - # middle of its headers. Make sure the order is preserved; it should - # be fifth. - msg = self._msgobj('msg_04.txt') - msg.set_boundary('BOUNDARY') - header, value = list(msg.items())[4] - eq(header.lower(), 'content-type') - eq(value, 'multipart/mixed; boundary="BOUNDARY"') - # And this one has no Content-Type: header at all. - msg = self._msgobj('msg_03.txt') - self.assertRaises(errors.HeaderParseError, - msg.set_boundary, 'BOUNDARY') - - def test_get_decoded_payload(self): - eq = self.assertEqual - msg = self._msgobj('msg_10.txt') - # The outer message is a multipart - eq(msg.get_payload(decode=True), None) - # Subpart 1 is 7bit encoded - eq(msg.get_payload(0).get_payload(decode=True), - 'This is a 7bit encoded message.\n') - # Subpart 2 is quopri - eq(msg.get_payload(1).get_payload(decode=True), - '\xa1This is a Quoted Printable encoded message!\n') - # Subpart 3 is base64 - eq(msg.get_payload(2).get_payload(decode=True), - 'This is a Base64 encoded message.') - # Subpart 4 has no Content-Transfer-Encoding: header. - eq(msg.get_payload(3).get_payload(decode=True), - 'This has no Content-Transfer-Encoding: header.\n') - - def test_get_decoded_uu_payload(self): - eq = self.assertEqual - msg = Message() - msg.set_payload('begin 666 -\n+:&5L;&\\@=V]R;&0 \n \nend\n') - for cte in ('x-uuencode', 'uuencode', 'uue', 'x-uue'): - msg['content-transfer-encoding'] = cte - eq(msg.get_payload(decode=True), 'hello world') - # Now try some bogus data - msg.set_payload('foo') - eq(msg.get_payload(decode=True), 'foo') - - def test_decoded_generator(self): - eq = self.assertEqual - msg = self._msgobj('msg_07.txt') - fp = openfile('msg_17.txt') - try: - text = fp.read() - finally: - fp.close() - s = StringIO() - g = DecodedGenerator(s) - g.flatten(msg) - eq(s.getvalue(), text) - - def test__contains__(self): - msg = Message() - msg['From'] = 'Me' - msg['to'] = 'You' - # Check for case insensitivity - self.assertTrue('from' in msg) - self.assertTrue('From' in msg) - self.assertTrue('FROM' in msg) - self.assertTrue('to' in msg) - self.assertTrue('To' in msg) - self.assertTrue('TO' in msg) - - def test_as_string(self): - eq = self.assertEqual - msg = self._msgobj('msg_01.txt') - fp = openfile('msg_01.txt') - try: - text = fp.read() - finally: - fp.close() - eq(text, msg.as_string()) - fullrepr = str(msg) - lines = fullrepr.split('\n') - self.assertTrue(lines[0].startswith('From ')) - eq(text, NL.join(lines[1:])) - - def test_bad_param(self): - msg = bongo.external.email.message_from_string("Content-Type: blarg; baz; boo\n") - self.assertEqual(msg.get_param('baz'), '') - - def test_missing_filename(self): - msg = bongo.external.email.message_from_string("From: foo\n") - self.assertEqual(msg.get_filename(), None) - - def test_bogus_filename(self): - msg = bongo.external.email.message_from_string( - "Content-Disposition: blarg; filename\n") - self.assertEqual(msg.get_filename(), '') - - def test_missing_boundary(self): - msg = bongo.external.email.message_from_string("From: foo\n") - self.assertEqual(msg.get_boundary(), None) - - def test_get_params(self): - eq = self.assertEqual - msg = bongo.external.email.message_from_string( - 'X-Header: foo=one; bar=two; baz=three\n') - eq(msg.get_params(header='x-header'), - [('foo', 'one'), ('bar', 'two'), ('baz', 'three')]) - msg = bongo.external.email.message_from_string( - 'X-Header: foo; bar=one; baz=two\n') - eq(msg.get_params(header='x-header'), - [('foo', ''), ('bar', 'one'), ('baz', 'two')]) - eq(msg.get_params(), None) - msg = bongo.external.email.message_from_string( - 'X-Header: foo; bar="one"; baz=two\n') - eq(msg.get_params(header='x-header'), - [('foo', ''), ('bar', 'one'), ('baz', 'two')]) - - def test_get_param_liberal(self): - msg = Message() - msg['Content-Type'] = 'Content-Type: Multipart/mixed; boundary = "CPIMSSMTPC06p5f3tG"' - self.assertEqual(msg.get_param('boundary'), 'CPIMSSMTPC06p5f3tG') - - def test_get_param(self): - eq = self.assertEqual - msg = bongo.external.email.message_from_string( - "X-Header: foo=one; bar=two; baz=three\n") - eq(msg.get_param('bar', header='x-header'), 'two') - eq(msg.get_param('quuz', header='x-header'), None) - eq(msg.get_param('quuz'), None) - msg = bongo.external.email.message_from_string( - 'X-Header: foo; bar="one"; baz=two\n') - eq(msg.get_param('foo', header='x-header'), '') - eq(msg.get_param('bar', header='x-header'), 'one') - eq(msg.get_param('baz', header='x-header'), 'two') - # XXX: We are not RFC-2045 compliant! We cannot parse: - # msg["Content-Type"] = 'text/plain; weird="hey; dolly? [you] @ <\\"home\\">?"' - # msg.get_param("weird") - # yet. - - def test_get_param_funky_continuation_lines(self): - msg = self._msgobj('msg_22.txt') - self.assertEqual(msg.get_payload(1).get_param('name'), 'wibble.JPG') - - def test_get_param_with_semis_in_quotes(self): - msg = bongo.external.email.message_from_string( - 'Content-Type: image/pjpeg; name="Jim&&Jill"\n') - self.assertEqual(msg.get_param('name'), 'Jim&&Jill') - self.assertEqual(msg.get_param('name', unquote=False), - '"Jim&&Jill"') - - def test_has_key(self): - msg = bongo.external.email.message_from_string('Header: exists') - self.assertTrue('header' in msg) - self.assertTrue('Header' in msg) - self.assertTrue('HEADER' in msg) - self.assertFalse('headeri' in msg) - - def test_set_param(self): - eq = self.assertEqual - msg = Message() - msg.set_param('charset', 'iso-2022-jp') - eq(msg.get_param('charset'), 'iso-2022-jp') - msg.set_param('importance', 'high value') - eq(msg.get_param('importance'), 'high value') - eq(msg.get_param('importance', unquote=False), '"high value"') - eq(msg.get_params(), [('text/plain', ''), - ('charset', 'iso-2022-jp'), - ('importance', 'high value')]) - eq(msg.get_params(unquote=False), [('text/plain', ''), - ('charset', '"iso-2022-jp"'), - ('importance', '"high value"')]) - msg.set_param('charset', 'iso-9999-xx', header='X-Jimmy') - eq(msg.get_param('charset', header='X-Jimmy'), 'iso-9999-xx') - - def test_del_param(self): - eq = self.assertEqual - msg = self._msgobj('msg_05.txt') - eq(msg.get_params(), - [('multipart/report', ''), ('report-type', 'delivery-status'), - ('boundary', 'D1690A7AC1.996856090/mail.example.com')]) - old_val = msg.get_param("report-type") - msg.del_param("report-type") - eq(msg.get_params(), - [('multipart/report', ''), - ('boundary', 'D1690A7AC1.996856090/mail.example.com')]) - msg.set_param("report-type", old_val) - eq(msg.get_params(), - [('multipart/report', ''), - ('boundary', 'D1690A7AC1.996856090/mail.example.com'), - ('report-type', old_val)]) - - def test_del_param_on_other_header(self): - msg = Message() - msg.add_header('Content-Disposition', 'attachment', filename='bud.gif') - msg.del_param('filename', 'content-disposition') - self.assertEqual(msg['content-disposition'], 'attachment') - - def test_set_type(self): - eq = self.assertEqual - msg = Message() - self.assertRaises(ValueError, msg.set_type, 'text') - msg.set_type('text/plain') - eq(msg['content-type'], 'text/plain') - msg.set_param('charset', 'us-ascii') - eq(msg['content-type'], 'text/plain; charset="us-ascii"') - msg.set_type('text/html') - eq(msg['content-type'], 'text/html; charset="us-ascii"') - - def test_set_type_on_other_header(self): - msg = Message() - msg['X-Content-Type'] = 'text/plain' - msg.set_type('application/octet-stream', 'X-Content-Type') - self.assertEqual(msg['x-content-type'], 'application/octet-stream') - - def test_get_content_type_missing(self): - msg = Message() - self.assertEqual(msg.get_content_type(), 'text/plain') - - def test_get_content_type_missing_with_default_type(self): - msg = Message() - msg.set_default_type('message/rfc822') - self.assertEqual(msg.get_content_type(), 'message/rfc822') - - def test_get_content_type_from_message_implicit(self): - msg = self._msgobj('msg_30.txt') - self.assertEqual(msg.get_payload(0).get_content_type(), - 'message/rfc822') - - def test_get_content_type_from_message_explicit(self): - msg = self._msgobj('msg_28.txt') - self.assertEqual(msg.get_payload(0).get_content_type(), - 'message/rfc822') - - def test_get_content_type_from_message_text_plain_implicit(self): - msg = self._msgobj('msg_03.txt') - self.assertEqual(msg.get_content_type(), 'text/plain') - - def test_get_content_type_from_message_text_plain_explicit(self): - msg = self._msgobj('msg_01.txt') - self.assertEqual(msg.get_content_type(), 'text/plain') - - def test_get_content_maintype_missing(self): - msg = Message() - self.assertEqual(msg.get_content_maintype(), 'text') - - def test_get_content_maintype_missing_with_default_type(self): - msg = Message() - msg.set_default_type('message/rfc822') - self.assertEqual(msg.get_content_maintype(), 'message') - - def test_get_content_maintype_from_message_implicit(self): - msg = self._msgobj('msg_30.txt') - self.assertEqual(msg.get_payload(0).get_content_maintype(), 'message') - - def test_get_content_maintype_from_message_explicit(self): - msg = self._msgobj('msg_28.txt') - self.assertEqual(msg.get_payload(0).get_content_maintype(), 'message') - - def test_get_content_maintype_from_message_text_plain_implicit(self): - msg = self._msgobj('msg_03.txt') - self.assertEqual(msg.get_content_maintype(), 'text') - - def test_get_content_maintype_from_message_text_plain_explicit(self): - msg = self._msgobj('msg_01.txt') - self.assertEqual(msg.get_content_maintype(), 'text') - - def test_get_content_subtype_missing(self): - msg = Message() - self.assertEqual(msg.get_content_subtype(), 'plain') - - def test_get_content_subtype_missing_with_default_type(self): - msg = Message() - msg.set_default_type('message/rfc822') - self.assertEqual(msg.get_content_subtype(), 'rfc822') - - def test_get_content_subtype_from_message_implicit(self): - msg = self._msgobj('msg_30.txt') - self.assertEqual(msg.get_payload(0).get_content_subtype(), 'rfc822') - - def test_get_content_subtype_from_message_explicit(self): - msg = self._msgobj('msg_28.txt') - self.assertEqual(msg.get_payload(0).get_content_subtype(), 'rfc822') - - def test_get_content_subtype_from_message_text_plain_implicit(self): - msg = self._msgobj('msg_03.txt') - self.assertEqual(msg.get_content_subtype(), 'plain') - - def test_get_content_subtype_from_message_text_plain_explicit(self): - msg = self._msgobj('msg_01.txt') - self.assertEqual(msg.get_content_subtype(), 'plain') - - def test_get_content_maintype_error(self): - msg = Message() - msg['Content-Type'] = 'no-slash-in-this-string' - self.assertEqual(msg.get_content_maintype(), 'text') - - def test_get_content_subtype_error(self): - msg = Message() - msg['Content-Type'] = 'no-slash-in-this-string' - self.assertEqual(msg.get_content_subtype(), 'plain') - - def test_replace_header(self): - eq = self.assertEqual - msg = Message() - msg.add_header('First', 'One') - msg.add_header('Second', 'Two') - msg.add_header('Third', 'Three') - eq(list(msg.keys()), ['First', 'Second', 'Third']) - eq(list(msg.values()), ['One', 'Two', 'Three']) - msg.replace_header('Second', 'Twenty') - eq(list(msg.keys()), ['First', 'Second', 'Third']) - eq(list(msg.values()), ['One', 'Twenty', 'Three']) - msg.add_header('First', 'Eleven') - msg.replace_header('First', 'One Hundred') - eq(list(msg.keys()), ['First', 'Second', 'Third', 'First']) - eq(list(msg.values()), ['One Hundred', 'Twenty', 'Three', 'Eleven']) - self.assertRaises(KeyError, msg.replace_header, 'Fourth', 'Missing') - - def test_broken_base64_payload(self): - x = 'AwDp0P7//y6LwKEAcPa/6Q=9' - msg = Message() - msg['content-type'] = 'audio/x-midi' - msg['content-transfer-encoding'] = 'base64' - msg.set_payload(x) - self.assertEqual(msg.get_payload(decode=True), x) - - - -# Test the bongo.external.email.encoders module -class TestEncoders(unittest.TestCase): - def test_encode_empty_payload(self): - eq = self.assertEqual - msg = Message() - msg.set_charset('us-ascii') - eq(msg['content-transfer-encoding'], '7bit') - - def test_default_cte(self): - eq = self.assertEqual - msg = MIMEText('hello world') - eq(msg['content-transfer-encoding'], '7bit') - - def test_default_cte(self): - eq = self.assertEqual - # With no explicit _charset its us-ascii, and all are 7-bit - msg = MIMEText('hello world') - eq(msg['content-transfer-encoding'], '7bit') - # Similar, but with 8-bit data - msg = MIMEText('hello \xf8 world') - eq(msg['content-transfer-encoding'], '8bit') - # And now with a different charset - msg = MIMEText('hello \xf8 world', _charset='iso-8859-1') - eq(msg['content-transfer-encoding'], 'quoted-printable') - - - -# Test long header wrapping -class TestLongHeaders(TestEmailBase): - def test_split_long_continuation(self): - eq = self.ndiffAssertEqual - msg = bongo.external.email.message_from_string("""\ -Subject: bug demonstration -\t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789 -\tmore text - -test -""") - sfp = StringIO() - g = Generator(sfp) - g.flatten(msg) - eq(sfp.getvalue(), """\ -Subject: bug demonstration -\t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789 -\tmore text - -test -""") - - def test_another_long_almost_unsplittable_header(self): - eq = self.ndiffAssertEqual - hstr = """\ -bug demonstration -\t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789 -\tmore text""" - h = Header(hstr, continuation_ws='\t') - eq(h.encode(), """\ -bug demonstration -\t12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789 -\tmore text""") - h = Header(hstr) - eq(h.encode(), """\ -bug demonstration - 12345678911234567892123456789312345678941234567895123456789612345678971234567898112345678911234567892123456789112345678911234567892123456789 - more text""") - - def test_long_nonstring(self): - eq = self.ndiffAssertEqual - g = Charset("iso-8859-1") - cz = Charset("iso-8859-2") - utf8 = Charset("utf-8") - g_head = "Die Mieter treten hier ein werden mit einem Foerderband komfortabel den Korridor entlang, an s\xfcdl\xfcndischen Wandgem\xe4lden vorbei, gegen die rotierenden Klingen bef\xf6rdert. " - cz_head = "Finan\xe8ni metropole se hroutily pod tlakem jejich d\xf9vtipu.. " - utf8_head = "\u6b63\u78ba\u306b\u8a00\u3046\u3068\u7ffb\u8a33\u306f\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u4e00\u90e8\u306f\u30c9\u30a4\u30c4\u8a9e\u3067\u3059\u304c\u3001\u3042\u3068\u306f\u3067\u305f\u3089\u3081\u3067\u3059\u3002\u5b9f\u969b\u306b\u306f\u300cWenn ist das Nunstuck git und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt gersput.\u300d\u3068\u8a00\u3063\u3066\u3044\u307e\u3059\u3002".encode("utf-8") - h = Header(g_head, g, header_name='Subject') - h.append(cz_head, cz) - h.append(utf8_head, utf8) - msg = Message() - msg['Subject'] = h - sfp = StringIO() - g = Generator(sfp) - g.flatten(msg) - eq(sfp.getvalue(), """\ -Subject: =?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerd?= - =?iso-8859-1?q?erband_komfortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndi?= - =?iso-8859-1?q?schen_Wandgem=E4lden_vorbei=2C_gegen_die_rotierenden_Kling?= - =?iso-8859-1?q?en_bef=F6rdert=2E_?= =?iso-8859-2?q?Finan=E8ni_met?= - =?iso-8859-2?q?ropole_se_hroutily_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= - =?utf-8?b?5q2j56K644Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE?= - =?utf-8?b?44G+44Gb44KT44CC5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB?= - =?utf-8?b?44GC44Go44Gv44Gn44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CM?= - =?utf-8?q?Wenn_ist_das_Nunstuck_git_und_Slotermeyer=3F_Ja!_Beiherhund_das?= - =?utf-8?b?IE9kZXIgZGllIEZsaXBwZXJ3YWxkdCBnZXJzcHV0LuOAjeOBqOiogOOBow==?= - =?utf-8?b?44Gm44GE44G+44GZ44CC?= - -""") - eq(h.encode(), """\ -=?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerd?= - =?iso-8859-1?q?erband_komfortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndi?= - =?iso-8859-1?q?schen_Wandgem=E4lden_vorbei=2C_gegen_die_rotierenden_Kling?= - =?iso-8859-1?q?en_bef=F6rdert=2E_?= =?iso-8859-2?q?Finan=E8ni_met?= - =?iso-8859-2?q?ropole_se_hroutily_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= - =?utf-8?b?5q2j56K644Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE?= - =?utf-8?b?44G+44Gb44KT44CC5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB?= - =?utf-8?b?44GC44Go44Gv44Gn44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CM?= - =?utf-8?q?Wenn_ist_das_Nunstuck_git_und_Slotermeyer=3F_Ja!_Beiherhund_das?= - =?utf-8?b?IE9kZXIgZGllIEZsaXBwZXJ3YWxkdCBnZXJzcHV0LuOAjeOBqOiogOOBow==?= - =?utf-8?b?44Gm44GE44G+44GZ44CC?=""") - - def test_long_header_encode(self): - eq = self.ndiffAssertEqual - h = Header('wasnipoop; giraffes="very-long-necked-animals"; ' - 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"', - header_name='X-Foobar-Spoink-Defrobnit') - eq(h.encode(), '''\ -wasnipoop; giraffes="very-long-necked-animals"; - spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"''') - - def test_long_header_encode_with_tab_continuation(self): - eq = self.ndiffAssertEqual - h = Header('wasnipoop; giraffes="very-long-necked-animals"; ' - 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"', - header_name='X-Foobar-Spoink-Defrobnit', - continuation_ws='\t') - eq(h.encode(), '''\ -wasnipoop; giraffes="very-long-necked-animals"; -\tspooge="yummy"; hippos="gargantuan"; marshmallows="gooey"''') - - def test_header_splitter(self): - eq = self.ndiffAssertEqual - msg = MIMEText('') - # It'd be great if we could use add_header() here, but that doesn't - # guarantee an order of the parameters. - msg['X-Foobar-Spoink-Defrobnit'] = ( - 'wasnipoop; giraffes="very-long-necked-animals"; ' - 'spooge="yummy"; hippos="gargantuan"; marshmallows="gooey"') - sfp = StringIO() - g = Generator(sfp) - g.flatten(msg) - eq(sfp.getvalue(), '''\ -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit -X-Foobar-Spoink-Defrobnit: wasnipoop; giraffes="very-long-necked-animals"; -\tspooge="yummy"; hippos="gargantuan"; marshmallows="gooey" - -''') - - def test_no_semis_header_splitter(self): - eq = self.ndiffAssertEqual - msg = Message() - msg['From'] = 'test@dom.ain' - msg['References'] = SPACE.join(['<%d@dom.ain>' % i for i in range(10)]) - msg.set_payload('Test') - sfp = StringIO() - g = Generator(sfp) - g.flatten(msg) - eq(sfp.getvalue(), """\ -From: test@dom.ain -References: <0@dom.ain> <1@dom.ain> <2@dom.ain> <3@dom.ain> <4@dom.ain> -\t<5@dom.ain> <6@dom.ain> <7@dom.ain> <8@dom.ain> <9@dom.ain> - -Test""") - - def test_no_split_long_header(self): - eq = self.ndiffAssertEqual - hstr = 'References: ' + 'x' * 80 - h = Header(hstr, continuation_ws='\t') - eq(h.encode(), """\ -References: xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx""") - - def test_splitting_multiple_long_lines(self): - eq = self.ndiffAssertEqual - hstr = """\ -from babylon.socal-raves.org (localhost [127.0.0.1]); by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; for ; Sat, 2 Feb 2002 17:00:06 -0800 (PST) -\tfrom babylon.socal-raves.org (localhost [127.0.0.1]); by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; for ; Sat, 2 Feb 2002 17:00:06 -0800 (PST) -\tfrom babylon.socal-raves.org (localhost [127.0.0.1]); by babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; for ; Sat, 2 Feb 2002 17:00:06 -0800 (PST) -""" - h = Header(hstr, continuation_ws='\t') - eq(h.encode(), """\ -from babylon.socal-raves.org (localhost [127.0.0.1]); -\tby babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; -\tfor ; -\tSat, 2 Feb 2002 17:00:06 -0800 (PST) -\tfrom babylon.socal-raves.org (localhost [127.0.0.1]); -\tby babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; -\tfor ; -\tSat, 2 Feb 2002 17:00:06 -0800 (PST) -\tfrom babylon.socal-raves.org (localhost [127.0.0.1]); -\tby babylon.socal-raves.org (Postfix) with ESMTP id B570E51B81; -\tfor ; -\tSat, 2 Feb 2002 17:00:06 -0800 (PST)""") - - def test_splitting_first_line_only_is_long(self): - eq = self.ndiffAssertEqual - hstr = """\ -from modemcable093.139-201-24.que.mc.videotron.ca ([24.201.139.93] helo=cthulhu.gerg.ca) -\tby kronos.mems-exchange.org with esmtp (Exim 4.05) -\tid 17k4h5-00034i-00 -\tfor test@mems-exchange.org; Wed, 28 Aug 2002 11:25:20 -0400""" - h = Header(hstr, maxlinelen=78, header_name='Received', - continuation_ws='\t') - eq(h.encode(), """\ -from modemcable093.139-201-24.que.mc.videotron.ca ([24.201.139.93] -\thelo=cthulhu.gerg.ca) -\tby kronos.mems-exchange.org with esmtp (Exim 4.05) -\tid 17k4h5-00034i-00 -\tfor test@mems-exchange.org; Wed, 28 Aug 2002 11:25:20 -0400""") - - def test_long_8bit_header(self): - eq = self.ndiffAssertEqual - msg = Message() - h = Header('Britische Regierung gibt', 'iso-8859-1', - header_name='Subject') - h.append('gr\xfcnes Licht f\xfcr Offshore-Windkraftprojekte') - msg['Subject'] = h - eq(msg.as_string(), """\ -Subject: =?iso-8859-1?q?Britische_Regierung_gibt?= =?iso-8859-1?q?gr=FCnes?= - =?iso-8859-1?q?_Licht_f=FCr_Offshore-Windkraftprojekte?= - -""") - - def test_long_8bit_header_no_charset(self): - eq = self.ndiffAssertEqual - msg = Message() - msg['Reply-To'] = 'Britische Regierung gibt gr\xfcnes Licht f\xfcr Offshore-Windkraftprojekte ' - eq(msg.as_string(), """\ -Reply-To: Britische Regierung gibt gr\xfcnes Licht f\xfcr Offshore-Windkraftprojekte - -""") - - def test_long_to_header(self): - eq = self.ndiffAssertEqual - to = '"Someone Test #A" ,,"Someone Test #B" , "Someone Test #C" , "Someone Test #D" ' - msg = Message() - msg['To'] = to - eq(msg.as_string(0), '''\ -To: "Someone Test #A" , , -\t"Someone Test #B" , -\t"Someone Test #C" , -\t"Someone Test #D" - -''') - - def test_long_line_after_append(self): - eq = self.ndiffAssertEqual - s = 'This is an example of string which has almost the limit of header length.' - h = Header(s) - h.append('Add another line.') - eq(h.encode(), """\ -This is an example of string which has almost the limit of header length. - Add another line.""") - - def test_shorter_line_with_append(self): - eq = self.ndiffAssertEqual - s = 'This is a shorter line.' - h = Header(s) - h.append('Add another sentence. (Surprise?)') - eq(h.encode(), - 'This is a shorter line. Add another sentence. (Surprise?)') - - def test_long_field_name(self): - eq = self.ndiffAssertEqual - fn = 'X-Very-Very-Very-Long-Header-Name' - gs = "Die Mieter treten hier ein werden mit einem Foerderband komfortabel den Korridor entlang, an s\xfcdl\xfcndischen Wandgem\xe4lden vorbei, gegen die rotierenden Klingen bef\xf6rdert. " - h = Header(gs, 'iso-8859-1', header_name=fn) - # BAW: this seems broken because the first line is too long - eq(h.encode(), """\ -=?iso-8859-1?q?Die_Mieter_treten_hier_?= - =?iso-8859-1?q?ein_werden_mit_einem_Foerderband_komfortabel_den_Korridor_?= - =?iso-8859-1?q?entlang=2C_an_s=FCdl=FCndischen_Wandgem=E4lden_vorbei=2C_g?= - =?iso-8859-1?q?egen_die_rotierenden_Klingen_bef=F6rdert=2E_?=""") - - def test_long_received_header(self): - h = 'from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) by hrothgar.la.mastaler.com (tmda-ofmipd) with ESMTP; Wed, 05 Mar 2003 18:10:18 -0700' - msg = Message() - msg['Received-1'] = Header(h, continuation_ws='\t') - msg['Received-2'] = h - self.assertEqual(msg.as_string(), """\ -Received-1: from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) by -\throthgar.la.mastaler.com (tmda-ofmipd) with ESMTP; -\tWed, 05 Mar 2003 18:10:18 -0700 -Received-2: from FOO.TLD (vizworld.acl.foo.tld [123.452.678.9]) by -\throthgar.la.mastaler.com (tmda-ofmipd) with ESMTP; -\tWed, 05 Mar 2003 18:10:18 -0700 - -""") - - def test_string_headerinst_eq(self): - h = '<15975.17901.207240.414604@sgigritzmann1.mathematik.tu-muenchen.de> (David Bremner\'s message of "Thu, 6 Mar 2003 13:58:21 +0100")' - msg = Message() - msg['Received-1'] = Header(h, header_name='Received-1', - continuation_ws='\t') - msg['Received-2'] = h - self.assertEqual(msg.as_string(), """\ -Received-1: <15975.17901.207240.414604@sgigritzmann1.mathematik.tu-muenchen.de> -\t(David Bremner's message of "Thu, 6 Mar 2003 13:58:21 +0100") -Received-2: <15975.17901.207240.414604@sgigritzmann1.mathematik.tu-muenchen.de> -\t(David Bremner's message of "Thu, 6 Mar 2003 13:58:21 +0100") - -""") - - def test_long_unbreakable_lines_with_continuation(self): - eq = self.ndiffAssertEqual - msg = Message() - t = """\ - iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9 - locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp""" - msg['Face-1'] = t - msg['Face-2'] = Header(t, header_name='Face-2') - eq(msg.as_string(), """\ -Face-1: iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9 -\tlocQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp -Face-2: iVBORw0KGgoAAAANSUhEUgAAADAAAAAwBAMAAAClLOS0AAAAGFBMVEUAAAAkHiJeRUIcGBi9 - locQDQ4zJykFBAXJfWDjAAACYUlEQVR4nF2TQY/jIAyFc6lydlG5x8Nyp1Y69wj1PN2I5gzp - -""") - - def test_another_long_multiline_header(self): - eq = self.ndiffAssertEqual - m = '''\ -Received: from siimage.com ([172.25.1.3]) by zima.siliconimage.com with Microsoft SMTPSVC(5.0.2195.4905); -\tWed, 16 Oct 2002 07:41:11 -0700''' - msg = bongo.external.email.message_from_string(m) - eq(msg.as_string(), '''\ -Received: from siimage.com ([172.25.1.3]) by zima.siliconimage.com with -\tMicrosoft SMTPSVC(5.0.2195.4905); Wed, 16 Oct 2002 07:41:11 -0700 - -''') - - def test_long_lines_with_different_header(self): - eq = self.ndiffAssertEqual - h = """\ -List-Unsubscribe: , - """ - msg = Message() - msg['List'] = h - msg['List'] = Header(h, header_name='List') - eq(msg.as_string(), """\ -List: List-Unsubscribe: , -\t -List: List-Unsubscribe: , - - -""") - - - -# Test mangling of "From " lines in the body of a message -class TestFromMangling(unittest.TestCase): - def setUp(self): - self.msg = Message() - self.msg['From'] = 'aaa@bbb.org' - self.msg.set_payload("""\ -From the desk of A.A.A.: -Blah blah blah -""") - - def test_mangled_from(self): - s = StringIO() - g = Generator(s, mangle_from_=True) - g.flatten(self.msg) - self.assertEqual(s.getvalue(), """\ -From: aaa@bbb.org - ->From the desk of A.A.A.: -Blah blah blah -""") - - def test_dont_mangle_from(self): - s = StringIO() - g = Generator(s, mangle_from_=False) - g.flatten(self.msg) - self.assertEqual(s.getvalue(), """\ -From: aaa@bbb.org - -From the desk of A.A.A.: -Blah blah blah -""") - - - -# Test the basic MIMEAudio class -class TestMIMEAudio(unittest.TestCase): - def setUp(self): - # Make sure we pick up the audiotest.au that lives in email/test/data. - # In Python, there's an audiotest.au living in Lib/test but that isn't - # included in some binary distros that don't include the test - # package. The trailing empty string on the .join() is significant - # since findfile() will do a dirname(). - datadir = os.path.join(os.path.dirname(landmark), 'data', '') - fp = open(findfile('audiotest.au', datadir), 'rb') - try: - self._audiodata = fp.read() - finally: - fp.close() - self._au = MIMEAudio(self._audiodata) - - def test_guess_minor_type(self): - self.assertEqual(self._au.get_content_type(), 'audio/basic') - - def test_encoding(self): - payload = self._au.get_payload() - self.assertEqual(base64.decodestring(payload), self._audiodata) - - def test_checkSetMinor(self): - au = MIMEAudio(self._audiodata, 'fish') - self.assertEqual(au.get_content_type(), 'audio/fish') - - def test_add_header(self): - eq = self.assertEqual - unless = self.assertTrue - self._au.add_header('Content-Disposition', 'attachment', - filename='audiotest.au') - eq(self._au['content-disposition'], - 'attachment; filename="audiotest.au"') - eq(self._au.get_params(header='content-disposition'), - [('attachment', ''), ('filename', 'audiotest.au')]) - eq(self._au.get_param('filename', header='content-disposition'), - 'audiotest.au') - missing = [] - eq(self._au.get_param('attachment', header='content-disposition'), '') - unless(self._au.get_param('foo', failobj=missing, - header='content-disposition') is missing) - # Try some missing stuff - unless(self._au.get_param('foobar', missing) is missing) - unless(self._au.get_param('attachment', missing, - header='foobar') is missing) - - - -# Test the basic MIMEImage class -class TestMIMEImage(unittest.TestCase): - def setUp(self): - fp = openfile('PyBanner048.gif') - try: - self._imgdata = fp.read() - finally: - fp.close() - self._im = MIMEImage(self._imgdata) - - def test_guess_minor_type(self): - self.assertEqual(self._im.get_content_type(), 'image/gif') - - def test_encoding(self): - payload = self._im.get_payload() - self.assertEqual(base64.decodestring(payload), self._imgdata) - - def test_checkSetMinor(self): - im = MIMEImage(self._imgdata, 'fish') - self.assertEqual(im.get_content_type(), 'image/fish') - - def test_add_header(self): - eq = self.assertEqual - unless = self.assertTrue - self._im.add_header('Content-Disposition', 'attachment', - filename='dingusfish.gif') - eq(self._im['content-disposition'], - 'attachment; filename="dingusfish.gif"') - eq(self._im.get_params(header='content-disposition'), - [('attachment', ''), ('filename', 'dingusfish.gif')]) - eq(self._im.get_param('filename', header='content-disposition'), - 'dingusfish.gif') - missing = [] - eq(self._im.get_param('attachment', header='content-disposition'), '') - unless(self._im.get_param('foo', failobj=missing, - header='content-disposition') is missing) - # Try some missing stuff - unless(self._im.get_param('foobar', missing) is missing) - unless(self._im.get_param('attachment', missing, - header='foobar') is missing) - - - -# Test the basic MIMEApplication class -class TestMIMEApplication(unittest.TestCase): - def test_headers(self): - eq = self.assertEqual - msg = MIMEApplication('\xfa\xfb\xfc\xfd\xfe\xff') - eq(msg.get_content_type(), 'application/octet-stream') - eq(msg['content-transfer-encoding'], 'base64') - - def test_body(self): - eq = self.assertEqual - bytes = '\xfa\xfb\xfc\xfd\xfe\xff' - msg = MIMEApplication(bytes) - eq(msg.get_payload(), '+vv8/f7/') - eq(msg.get_payload(decode=True), bytes) - - - -# Test the basic MIMEText class -class TestMIMEText(unittest.TestCase): - def setUp(self): - self._msg = MIMEText('hello there') - - def test_types(self): - eq = self.assertEqual - unless = self.assertTrue - eq(self._msg.get_content_type(), 'text/plain') - eq(self._msg.get_param('charset'), 'us-ascii') - missing = [] - unless(self._msg.get_param('foobar', missing) is missing) - unless(self._msg.get_param('charset', missing, header='foobar') - is missing) - - def test_payload(self): - self.assertEqual(self._msg.get_payload(), 'hello there') - self.assertTrue(not self._msg.is_multipart()) - - def test_charset(self): - eq = self.assertEqual - msg = MIMEText('hello there', _charset='us-ascii') - eq(msg.get_charset().input_charset, 'us-ascii') - eq(msg['content-type'], 'text/plain; charset="us-ascii"') - - - -# Test complicated multipart/* messages -class TestMultipart(TestEmailBase): - def setUp(self): - fp = openfile('PyBanner048.gif') - try: - data = fp.read() - finally: - fp.close() - - container = MIMEBase('multipart', 'mixed', boundary='BOUNDARY') - image = MIMEImage(data, name='dingusfish.gif') - image.add_header('content-disposition', 'attachment', - filename='dingusfish.gif') - intro = MIMEText('''\ -Hi there, - -This is the dingus fish. -''') - container.attach(intro) - container.attach(image) - container['From'] = 'Barry ' - container['To'] = 'Dingus Lovers ' - container['Subject'] = 'Here is your dingus fish' - - now = 987809702.54848599 - timetuple = time.localtime(now) - if timetuple[-1] == 0: - tzsecs = time.timezone - else: - tzsecs = time.altzone - if tzsecs > 0: - sign = '-' - else: - sign = '+' - tzoffset = ' %s%04d' % (sign, tzsecs / 36) - container['Date'] = time.strftime( - '%a, %d %b %Y %H:%M:%S', - time.localtime(now)) + tzoffset - self._msg = container - self._im = image - self._txt = intro - - def test_hierarchy(self): - # convenience - eq = self.assertEqual - unless = self.assertTrue - raises = self.assertRaises - # tests - m = self._msg - unless(m.is_multipart()) - eq(m.get_content_type(), 'multipart/mixed') - eq(len(m.get_payload()), 2) - raises(IndexError, m.get_payload, 2) - m0 = m.get_payload(0) - m1 = m.get_payload(1) - unless(m0 is self._txt) - unless(m1 is self._im) - eq(m.get_payload(), [m0, m1]) - unless(not m0.is_multipart()) - unless(not m1.is_multipart()) - - def test_empty_multipart_idempotent(self): - text = """\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - - ---BOUNDARY - - ---BOUNDARY-- -""" - msg = Parser().parsestr(text) - self.ndiffAssertEqual(text, msg.as_string()) - - def test_no_parts_in_a_multipart_with_none_epilogue(self): - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.set_boundary('BOUNDARY') - self.ndiffAssertEqual(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - ---BOUNDARY - ---BOUNDARY--''') - - def test_no_parts_in_a_multipart_with_empty_epilogue(self): - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.preamble = '' - outer.epilogue = '' - outer.set_boundary('BOUNDARY') - self.ndiffAssertEqual(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - - ---BOUNDARY - ---BOUNDARY-- -''') - - def test_one_part_in_a_multipart(self): - eq = self.ndiffAssertEqual - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.set_boundary('BOUNDARY') - msg = MIMEText('hello world') - outer.attach(msg) - eq(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -hello world ---BOUNDARY--''') - - def test_seq_parts_in_a_multipart_with_empty_preamble(self): - eq = self.ndiffAssertEqual - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.preamble = '' - msg = MIMEText('hello world') - outer.attach(msg) - outer.set_boundary('BOUNDARY') - eq(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -hello world ---BOUNDARY--''') - - - def test_seq_parts_in_a_multipart_with_none_preamble(self): - eq = self.ndiffAssertEqual - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.preamble = None - msg = MIMEText('hello world') - outer.attach(msg) - outer.set_boundary('BOUNDARY') - eq(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -hello world ---BOUNDARY--''') - - - def test_seq_parts_in_a_multipart_with_none_epilogue(self): - eq = self.ndiffAssertEqual - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.epilogue = None - msg = MIMEText('hello world') - outer.attach(msg) - outer.set_boundary('BOUNDARY') - eq(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -hello world ---BOUNDARY--''') - - - def test_seq_parts_in_a_multipart_with_empty_epilogue(self): - eq = self.ndiffAssertEqual - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.epilogue = '' - msg = MIMEText('hello world') - outer.attach(msg) - outer.set_boundary('BOUNDARY') - eq(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -hello world ---BOUNDARY-- -''') - - - def test_seq_parts_in_a_multipart_with_nl_epilogue(self): - eq = self.ndiffAssertEqual - outer = MIMEBase('multipart', 'mixed') - outer['Subject'] = 'A subject' - outer['To'] = 'aperson@dom.ain' - outer['From'] = 'bperson@dom.ain' - outer.epilogue = '\n' - msg = MIMEText('hello world') - outer.attach(msg) - outer.set_boundary('BOUNDARY') - eq(outer.as_string(), '''\ -Content-Type: multipart/mixed; boundary="BOUNDARY" -MIME-Version: 1.0 -Subject: A subject -To: aperson@dom.ain -From: bperson@dom.ain - ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -hello world ---BOUNDARY-- - -''') - - def test_message_external_body(self): - eq = self.assertEqual - msg = self._msgobj('msg_36.txt') - eq(len(msg.get_payload()), 2) - msg1 = msg.get_payload(1) - eq(msg1.get_content_type(), 'multipart/alternative') - eq(len(msg1.get_payload()), 2) - for subpart in msg1.get_payload(): - eq(subpart.get_content_type(), 'message/external-body') - eq(len(subpart.get_payload()), 1) - subsubpart = subpart.get_payload(0) - eq(subsubpart.get_content_type(), 'text/plain') - - def test_double_boundary(self): - # msg_37.txt is a multipart that contains two dash-boundary's in a - # row. Our interpretation of RFC 2046 calls for ignoring the second - # and subsequent boundaries. - msg = self._msgobj('msg_37.txt') - self.assertEqual(len(msg.get_payload()), 3) - - def test_nested_inner_contains_outer_boundary(self): - eq = self.ndiffAssertEqual - # msg_38.txt has an inner part that contains outer boundaries. My - # interpretation of RFC 2046 (based on sections 5.1 and 5.1.2) say - # these are illegal and should be interpreted as unterminated inner - # parts. - msg = self._msgobj('msg_38.txt') - sfp = StringIO() - iterators._structure(msg, sfp) - eq(sfp.getvalue(), """\ -multipart/mixed - multipart/mixed - multipart/alternative - text/plain - text/plain - text/plain - text/plain -""") - - def test_nested_with_same_boundary(self): - eq = self.ndiffAssertEqual - # msg 39.txt is similarly evil in that it's got inner parts that use - # the same boundary as outer parts. Again, I believe the way this is - # parsed is closest to the spirit of RFC 2046 - msg = self._msgobj('msg_39.txt') - sfp = StringIO() - iterators._structure(msg, sfp) - eq(sfp.getvalue(), """\ -multipart/mixed - multipart/mixed - multipart/alternative - application/octet-stream - application/octet-stream - text/plain -""") - - def test_boundary_in_non_multipart(self): - msg = self._msgobj('msg_40.txt') - self.assertEqual(msg.as_string(), '''\ -MIME-Version: 1.0 -Content-Type: text/html; boundary="--961284236552522269" - -----961284236552522269 -Content-Type: text/html; -Content-Transfer-Encoding: 7Bit - - - -----961284236552522269-- -''') - - def test_boundary_with_leading_space(self): - eq = self.assertEqual - msg = bongo.external.email.message_from_string('''\ -MIME-Version: 1.0 -Content-Type: multipart/mixed; boundary=" XXXX" - --- XXXX -Content-Type: text/plain - - --- XXXX -Content-Type: text/plain - --- XXXX-- -''') - self.assertTrue(msg.is_multipart()) - eq(msg.get_boundary(), ' XXXX') - eq(len(msg.get_payload()), 2) - - def test_boundary_without_trailing_newline(self): - m = Parser().parsestr("""\ -Content-Type: multipart/mixed; boundary="===============0012394164==" -MIME-Version: 1.0 - ---===============0012394164== -Content-Type: image/file1.jpg -MIME-Version: 1.0 -Content-Transfer-Encoding: base64 - -YXNkZg== ---===============0012394164==--""") - self.assertEqual(m.get_payload(0).get_payload(), 'YXNkZg==') - - - -# Test some badly formatted messages -class TestNonConformant(TestEmailBase): - def test_parse_missing_minor_type(self): - eq = self.assertEqual - msg = self._msgobj('msg_14.txt') - eq(msg.get_content_type(), 'text/plain') - eq(msg.get_content_maintype(), 'text') - eq(msg.get_content_subtype(), 'plain') - - def test_same_boundary_inner_outer(self): - unless = self.assertTrue - msg = self._msgobj('msg_15.txt') - # XXX We can probably eventually do better - inner = msg.get_payload(0) - unless(hasattr(inner, 'defects')) - self.assertEqual(len(inner.defects), 1) - unless(isinstance(inner.defects[0], - errors.StartBoundaryNotFoundDefect)) - - def test_multipart_no_boundary(self): - unless = self.assertTrue - msg = self._msgobj('msg_25.txt') - unless(isinstance(msg.get_payload(), str)) - self.assertEqual(len(msg.defects), 2) - unless(isinstance(msg.defects[0], errors.NoBoundaryInMultipartDefect)) - unless(isinstance(msg.defects[1], - errors.MultipartInvariantViolationDefect)) - - def test_invalid_content_type(self): - eq = self.assertEqual - neq = self.ndiffAssertEqual - msg = Message() - # RFC 2045, $5.2 says invalid yields text/plain - msg['Content-Type'] = 'text' - eq(msg.get_content_maintype(), 'text') - eq(msg.get_content_subtype(), 'plain') - eq(msg.get_content_type(), 'text/plain') - # Clear the old value and try something /really/ invalid - del msg['content-type'] - msg['Content-Type'] = 'foo' - eq(msg.get_content_maintype(), 'text') - eq(msg.get_content_subtype(), 'plain') - eq(msg.get_content_type(), 'text/plain') - # Still, make sure that the message is idempotently generated - s = StringIO() - g = Generator(s) - g.flatten(msg) - neq(s.getvalue(), 'Content-Type: foo\n\n') - - def test_no_start_boundary(self): - eq = self.ndiffAssertEqual - msg = self._msgobj('msg_31.txt') - eq(msg.get_payload(), """\ ---BOUNDARY -Content-Type: text/plain - -message 1 - ---BOUNDARY -Content-Type: text/plain - -message 2 - ---BOUNDARY-- -""") - - def test_no_separating_blank_line(self): - eq = self.ndiffAssertEqual - msg = self._msgobj('msg_35.txt') - eq(msg.as_string(), """\ -From: aperson@dom.ain -To: bperson@dom.ain -Subject: here's something interesting - -counter to RFC 2822, there's no separating newline here -""") - - def test_lying_multipart(self): - unless = self.assertTrue - msg = self._msgobj('msg_41.txt') - unless(hasattr(msg, 'defects')) - self.assertEqual(len(msg.defects), 2) - unless(isinstance(msg.defects[0], errors.NoBoundaryInMultipartDefect)) - unless(isinstance(msg.defects[1], - errors.MultipartInvariantViolationDefect)) - - def test_missing_start_boundary(self): - outer = self._msgobj('msg_42.txt') - # The message structure is: - # - # multipart/mixed - # text/plain - # message/rfc822 - # multipart/mixed [*] - # - # [*] This message is missing its start boundary - bad = outer.get_payload(1).get_payload(0) - self.assertEqual(len(bad.defects), 1) - self.assertTrue(isinstance(bad.defects[0], - errors.StartBoundaryNotFoundDefect)) - - - -# Test RFC 2047 header encoding and decoding -class TestRFC2047(unittest.TestCase): - def test_rfc2047_multiline(self): - eq = self.assertEqual - s = """Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz - foo bar =?mac-iceland?q?r=8Aksm=9Arg=8Cs?=""" - dh = decode_header(s) - eq(dh, [ - ('Re:', None), - ('r\x8aksm\x9arg\x8cs', 'mac-iceland'), - ('baz foo bar', None), - ('r\x8aksm\x9arg\x8cs', 'mac-iceland')]) - eq(str(make_header(dh)), - """Re: =?mac-iceland?q?r=8Aksm=9Arg=8Cs?= baz foo bar - =?mac-iceland?q?r=8Aksm=9Arg=8Cs?=""") - - def test_whitespace_eater_unicode(self): - eq = self.assertEqual - s = '=?ISO-8859-1?Q?Andr=E9?= Pirard ' - dh = decode_header(s) - eq(dh, [('Andr\xe9', 'iso-8859-1'), ('Pirard ', None)]) - hu = str(make_header(dh)).encode('latin-1') - eq(hu, 'Andr\xe9 Pirard ') - - def test_whitespace_eater_unicode_2(self): - eq = self.assertEqual - s = 'The =?iso-8859-1?b?cXVpY2sgYnJvd24gZm94?= jumped over the =?iso-8859-1?b?bGF6eSBkb2c=?=' - dh = decode_header(s) - eq(dh, [('The', None), ('quick brown fox', 'iso-8859-1'), - ('jumped over the', None), ('lazy dog', 'iso-8859-1')]) - hu = make_header(dh).__unicode__() - eq(hu, 'The quick brown fox jumped over the lazy dog') - - - -# Test the MIMEMessage class -class TestMIMEMessage(TestEmailBase): - def setUp(self): - fp = openfile('msg_11.txt') - try: - self._text = fp.read() - finally: - fp.close() - - def test_type_error(self): - self.assertRaises(TypeError, MIMEMessage, 'a plain string') - - def test_valid_argument(self): - eq = self.assertEqual - unless = self.assertTrue - subject = 'A sub-message' - m = Message() - m['Subject'] = subject - r = MIMEMessage(m) - eq(r.get_content_type(), 'message/rfc822') - payload = r.get_payload() - unless(isinstance(payload, list)) - eq(len(payload), 1) - subpart = payload[0] - unless(subpart is m) - eq(subpart['subject'], subject) - - def test_bad_multipart(self): - eq = self.assertEqual - msg1 = Message() - msg1['Subject'] = 'subpart 1' - msg2 = Message() - msg2['Subject'] = 'subpart 2' - r = MIMEMessage(msg1) - self.assertRaises(errors.MultipartConversionError, r.attach, msg2) - - def test_generate(self): - # First craft the message to be encapsulated - m = Message() - m['Subject'] = 'An enclosed message' - m.set_payload('Here is the body of the message.\n') - r = MIMEMessage(m) - r['Subject'] = 'The enclosing message' - s = StringIO() - g = Generator(s) - g.flatten(r) - self.assertEqual(s.getvalue(), """\ -Content-Type: message/rfc822 -MIME-Version: 1.0 -Subject: The enclosing message - -Subject: An enclosed message - -Here is the body of the message. -""") - - def test_parse_message_rfc822(self): - eq = self.assertEqual - unless = self.assertTrue - msg = self._msgobj('msg_11.txt') - eq(msg.get_content_type(), 'message/rfc822') - payload = msg.get_payload() - unless(isinstance(payload, list)) - eq(len(payload), 1) - submsg = payload[0] - self.assertTrue(isinstance(submsg, Message)) - eq(submsg['subject'], 'An enclosed message') - eq(submsg.get_payload(), 'Here is the body of the message.\n') - - def test_dsn(self): - eq = self.assertEqual - unless = self.assertTrue - # msg 16 is a Delivery Status Notification, see RFC 1894 - msg = self._msgobj('msg_16.txt') - eq(msg.get_content_type(), 'multipart/report') - unless(msg.is_multipart()) - eq(len(msg.get_payload()), 3) - # Subpart 1 is a text/plain, human readable section - subpart = msg.get_payload(0) - eq(subpart.get_content_type(), 'text/plain') - eq(subpart.get_payload(), """\ -This report relates to a message you sent with the following header fields: - - Message-id: <002001c144a6$8752e060$56104586@oxy.edu> - Date: Sun, 23 Sep 2001 20:10:55 -0700 - From: "Ian T. Henry" - To: SoCal Raves - Subject: [scr] yeah for Ians!! - -Your message cannot be delivered to the following recipients: - - Recipient address: jangel1@cougar.noc.ucla.edu - Reason: recipient reached disk quota - -""") - # Subpart 2 contains the machine parsable DSN information. It - # consists of two blocks of headers, represented by two nested Message - # objects. - subpart = msg.get_payload(1) - eq(subpart.get_content_type(), 'message/delivery-status') - eq(len(subpart.get_payload()), 2) - # message/delivery-status should treat each block as a bunch of - # headers, i.e. a bunch of Message objects. - dsn1 = subpart.get_payload(0) - unless(isinstance(dsn1, Message)) - eq(dsn1['original-envelope-id'], '0GK500B4HD0888@cougar.noc.ucla.edu') - eq(dsn1.get_param('dns', header='reporting-mta'), '') - # Try a missing one - eq(dsn1.get_param('nsd', header='reporting-mta'), None) - dsn2 = subpart.get_payload(1) - unless(isinstance(dsn2, Message)) - eq(dsn2['action'], 'failed') - eq(dsn2.get_params(header='original-recipient'), - [('rfc822', ''), ('jangel1@cougar.noc.ucla.edu', '')]) - eq(dsn2.get_param('rfc822', header='final-recipient'), '') - # Subpart 3 is the original message - subpart = msg.get_payload(2) - eq(subpart.get_content_type(), 'message/rfc822') - payload = subpart.get_payload() - unless(isinstance(payload, list)) - eq(len(payload), 1) - subsubpart = payload[0] - unless(isinstance(subsubpart, Message)) - eq(subsubpart.get_content_type(), 'text/plain') - eq(subsubpart['message-id'], - '<002001c144a6$8752e060$56104586@oxy.edu>') - - def test_epilogue(self): - eq = self.ndiffAssertEqual - fp = openfile('msg_21.txt') - try: - text = fp.read() - finally: - fp.close() - msg = Message() - msg['From'] = 'aperson@dom.ain' - msg['To'] = 'bperson@dom.ain' - msg['Subject'] = 'Test' - msg.preamble = 'MIME message' - msg.epilogue = 'End of MIME message\n' - msg1 = MIMEText('One') - msg2 = MIMEText('Two') - msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY') - msg.attach(msg1) - msg.attach(msg2) - sfp = StringIO() - g = Generator(sfp) - g.flatten(msg) - eq(sfp.getvalue(), text) - - def test_no_nl_preamble(self): - eq = self.ndiffAssertEqual - msg = Message() - msg['From'] = 'aperson@dom.ain' - msg['To'] = 'bperson@dom.ain' - msg['Subject'] = 'Test' - msg.preamble = 'MIME message' - msg.epilogue = '' - msg1 = MIMEText('One') - msg2 = MIMEText('Two') - msg.add_header('Content-Type', 'multipart/mixed', boundary='BOUNDARY') - msg.attach(msg1) - msg.attach(msg2) - eq(msg.as_string(), """\ -From: aperson@dom.ain -To: bperson@dom.ain -Subject: Test -Content-Type: multipart/mixed; boundary="BOUNDARY" - -MIME message ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -One ---BOUNDARY -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -Two ---BOUNDARY-- -""") - - def test_default_type(self): - eq = self.assertEqual - fp = openfile('msg_30.txt') - try: - msg = bongo.external.email.message_from_file(fp) - finally: - fp.close() - container1 = msg.get_payload(0) - eq(container1.get_default_type(), 'message/rfc822') - eq(container1.get_content_type(), 'message/rfc822') - container2 = msg.get_payload(1) - eq(container2.get_default_type(), 'message/rfc822') - eq(container2.get_content_type(), 'message/rfc822') - container1a = container1.get_payload(0) - eq(container1a.get_default_type(), 'text/plain') - eq(container1a.get_content_type(), 'text/plain') - container2a = container2.get_payload(0) - eq(container2a.get_default_type(), 'text/plain') - eq(container2a.get_content_type(), 'text/plain') - - def test_default_type_with_explicit_container_type(self): - eq = self.assertEqual - fp = openfile('msg_28.txt') - try: - msg = bongo.external.email.message_from_file(fp) - finally: - fp.close() - container1 = msg.get_payload(0) - eq(container1.get_default_type(), 'message/rfc822') - eq(container1.get_content_type(), 'message/rfc822') - container2 = msg.get_payload(1) - eq(container2.get_default_type(), 'message/rfc822') - eq(container2.get_content_type(), 'message/rfc822') - container1a = container1.get_payload(0) - eq(container1a.get_default_type(), 'text/plain') - eq(container1a.get_content_type(), 'text/plain') - container2a = container2.get_payload(0) - eq(container2a.get_default_type(), 'text/plain') - eq(container2a.get_content_type(), 'text/plain') - - def test_default_type_non_parsed(self): - eq = self.assertEqual - neq = self.ndiffAssertEqual - # Set up container - container = MIMEMultipart('digest', 'BOUNDARY') - container.epilogue = '' - # Set up subparts - subpart1a = MIMEText('message 1\n') - subpart2a = MIMEText('message 2\n') - subpart1 = MIMEMessage(subpart1a) - subpart2 = MIMEMessage(subpart2a) - container.attach(subpart1) - container.attach(subpart2) - eq(subpart1.get_content_type(), 'message/rfc822') - eq(subpart1.get_default_type(), 'message/rfc822') - eq(subpart2.get_content_type(), 'message/rfc822') - eq(subpart2.get_default_type(), 'message/rfc822') - neq(container.as_string(0), '''\ -Content-Type: multipart/digest; boundary="BOUNDARY" -MIME-Version: 1.0 - ---BOUNDARY -Content-Type: message/rfc822 -MIME-Version: 1.0 - -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -message 1 - ---BOUNDARY -Content-Type: message/rfc822 -MIME-Version: 1.0 - -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -message 2 - ---BOUNDARY-- -''') - del subpart1['content-type'] - del subpart1['mime-version'] - del subpart2['content-type'] - del subpart2['mime-version'] - eq(subpart1.get_content_type(), 'message/rfc822') - eq(subpart1.get_default_type(), 'message/rfc822') - eq(subpart2.get_content_type(), 'message/rfc822') - eq(subpart2.get_default_type(), 'message/rfc822') - neq(container.as_string(0), '''\ -Content-Type: multipart/digest; boundary="BOUNDARY" -MIME-Version: 1.0 - ---BOUNDARY - -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -message 1 - ---BOUNDARY - -Content-Type: text/plain; charset="us-ascii" -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit - -message 2 - ---BOUNDARY-- -''') - - def test_mime_attachments_in_constructor(self): - eq = self.assertEqual - text1 = MIMEText('') - text2 = MIMEText('') - msg = MIMEMultipart(_subparts=(text1, text2)) - eq(len(msg.get_payload()), 2) - eq(msg.get_payload(0), text1) - eq(msg.get_payload(1), text2) - - - -# A general test of parser->model->generator idempotency. IOW, read a message -# in, parse it into a message object tree, then without touching the tree, -# regenerate the plain text. The original text and the transformed text -# should be identical. Note: that we ignore the Unix-From since that may -# contain a changed date. -class TestIdempotent(TestEmailBase): - def _msgobj(self, filename): - fp = openfile(filename) - try: - data = fp.read() - finally: - fp.close() - msg = bongo.external.email.message_from_string(data) - return msg, data - - def _idempotent(self, msg, text): - eq = self.ndiffAssertEqual - s = StringIO() - g = Generator(s, maxheaderlen=0) - g.flatten(msg) - eq(text, s.getvalue()) - - def test_parse_text_message(self): - eq = self.assertEqual - msg, text = self._msgobj('msg_01.txt') - eq(msg.get_content_type(), 'text/plain') - eq(msg.get_content_maintype(), 'text') - eq(msg.get_content_subtype(), 'plain') - eq(msg.get_params()[1], ('charset', 'us-ascii')) - eq(msg.get_param('charset'), 'us-ascii') - eq(msg.preamble, None) - eq(msg.epilogue, None) - self._idempotent(msg, text) - - def test_parse_untyped_message(self): - eq = self.assertEqual - msg, text = self._msgobj('msg_03.txt') - eq(msg.get_content_type(), 'text/plain') - eq(msg.get_params(), None) - eq(msg.get_param('charset'), None) - self._idempotent(msg, text) - - def test_simple_multipart(self): - msg, text = self._msgobj('msg_04.txt') - self._idempotent(msg, text) - - def test_MIME_digest(self): - msg, text = self._msgobj('msg_02.txt') - self._idempotent(msg, text) - - def test_long_header(self): - msg, text = self._msgobj('msg_27.txt') - self._idempotent(msg, text) - - def test_MIME_digest_with_part_headers(self): - msg, text = self._msgobj('msg_28.txt') - self._idempotent(msg, text) - - def test_mixed_with_image(self): - msg, text = self._msgobj('msg_06.txt') - self._idempotent(msg, text) - - def test_multipart_report(self): - msg, text = self._msgobj('msg_05.txt') - self._idempotent(msg, text) - - def test_dsn(self): - msg, text = self._msgobj('msg_16.txt') - self._idempotent(msg, text) - - def test_preamble_epilogue(self): - msg, text = self._msgobj('msg_21.txt') - self._idempotent(msg, text) - - def test_multipart_one_part(self): - msg, text = self._msgobj('msg_23.txt') - self._idempotent(msg, text) - - def test_multipart_no_parts(self): - msg, text = self._msgobj('msg_24.txt') - self._idempotent(msg, text) - - def test_no_start_boundary(self): - msg, text = self._msgobj('msg_31.txt') - self._idempotent(msg, text) - - def test_rfc2231_charset(self): - msg, text = self._msgobj('msg_32.txt') - self._idempotent(msg, text) - - def test_more_rfc2231_parameters(self): - msg, text = self._msgobj('msg_33.txt') - self._idempotent(msg, text) - - def test_text_plain_in_a_multipart_digest(self): - msg, text = self._msgobj('msg_34.txt') - self._idempotent(msg, text) - - def test_nested_multipart_mixeds(self): - msg, text = self._msgobj('msg_12a.txt') - self._idempotent(msg, text) - - def test_message_external_body_idempotent(self): - msg, text = self._msgobj('msg_36.txt') - self._idempotent(msg, text) - - def test_content_type(self): - eq = self.assertEqual - unless = self.assertTrue - # Get a message object and reset the seek pointer for other tests - msg, text = self._msgobj('msg_05.txt') - eq(msg.get_content_type(), 'multipart/report') - # Test the Content-Type: parameters - params = {} - for pk, pv in msg.get_params(): - params[pk] = pv - eq(params['report-type'], 'delivery-status') - eq(params['boundary'], 'D1690A7AC1.996856090/mail.example.com') - eq(msg.preamble, 'This is a MIME-encapsulated message.\n') - eq(msg.epilogue, '\n') - eq(len(msg.get_payload()), 3) - # Make sure the subparts are what we expect - msg1 = msg.get_payload(0) - eq(msg1.get_content_type(), 'text/plain') - eq(msg1.get_payload(), 'Yadda yadda yadda\n') - msg2 = msg.get_payload(1) - eq(msg2.get_content_type(), 'text/plain') - eq(msg2.get_payload(), 'Yadda yadda yadda\n') - msg3 = msg.get_payload(2) - eq(msg3.get_content_type(), 'message/rfc822') - self.assertTrue(isinstance(msg3, Message)) - payload = msg3.get_payload() - unless(isinstance(payload, list)) - eq(len(payload), 1) - msg4 = payload[0] - unless(isinstance(msg4, Message)) - eq(msg4.get_payload(), 'Yadda yadda yadda\n') - - def test_parser(self): - eq = self.assertEqual - unless = self.assertTrue - msg, text = self._msgobj('msg_06.txt') - # Check some of the outer headers - eq(msg.get_content_type(), 'message/rfc822') - # Make sure the payload is a list of exactly one sub-Message, and that - # that submessage has a type of text/plain - payload = msg.get_payload() - unless(isinstance(payload, list)) - eq(len(payload), 1) - msg1 = payload[0] - self.assertTrue(isinstance(msg1, Message)) - eq(msg1.get_content_type(), 'text/plain') - self.assertTrue(isinstance(msg1.get_payload(), str)) - eq(msg1.get_payload(), '\n') - - - -# Test various other bits of the package's functionality -class TestMiscellaneous(TestEmailBase): - def test_message_from_string(self): - fp = openfile('msg_01.txt') - try: - text = fp.read() - finally: - fp.close() - msg = bongo.external.email.message_from_string(text) - s = StringIO() - # Don't wrap/continue long headers since we're trying to test - # idempotency. - g = Generator(s, maxheaderlen=0) - g.flatten(msg) - self.assertEqual(text, s.getvalue()) - - def test_message_from_file(self): - fp = openfile('msg_01.txt') - try: - text = fp.read() - fp.seek(0) - msg = bongo.external.email.message_from_file(fp) - s = StringIO() - # Don't wrap/continue long headers since we're trying to test - # idempotency. - g = Generator(s, maxheaderlen=0) - g.flatten(msg) - self.assertEqual(text, s.getvalue()) - finally: - fp.close() - - def test_message_from_string_with_class(self): - unless = self.assertTrue - fp = openfile('msg_01.txt') - try: - text = fp.read() - finally: - fp.close() - # Create a subclass - class MyMessage(Message): - pass - - msg = bongo.external.email.message_from_string(text, MyMessage) - unless(isinstance(msg, MyMessage)) - # Try something more complicated - fp = openfile('msg_02.txt') - try: - text = fp.read() - finally: - fp.close() - msg = bongo.external.email.message_from_string(text, MyMessage) - for subpart in msg.walk(): - unless(isinstance(subpart, MyMessage)) - - def test_message_from_file_with_class(self): - unless = self.assertTrue - # Create a subclass - class MyMessage(Message): - pass - - fp = openfile('msg_01.txt') - try: - msg = bongo.external.email.message_from_file(fp, MyMessage) - finally: - fp.close() - unless(isinstance(msg, MyMessage)) - # Try something more complicated - fp = openfile('msg_02.txt') - try: - msg = bongo.external.email.message_from_file(fp, MyMessage) - finally: - fp.close() - for subpart in msg.walk(): - unless(isinstance(subpart, MyMessage)) - - def test__all__(self): - module = __import__('email') - # Can't use sorted() here due to Python 2.3 compatibility - all = module.__all__[:] - all.sort() - self.assertEqual(all, [ - # Old names - 'Charset', 'Encoders', 'Errors', 'Generator', - 'Header', 'Iterators', 'MIMEAudio', 'MIMEBase', - 'MIMEImage', 'MIMEMessage', 'MIMEMultipart', - 'MIMENonMultipart', 'MIMEText', 'Message', - 'Parser', 'Utils', 'base64MIME', - # new names - 'base64mime', 'charset', 'encoders', 'errors', 'generator', - 'header', 'iterators', 'message', 'message_from_file', - 'message_from_string', 'mime', 'parser', - 'quopriMIME', 'quoprimime', 'utils', - ]) - - def test_formatdate(self): - now = time.time() - self.assertEqual(utils.parsedate(utils.formatdate(now))[:6], - time.gmtime(now)[:6]) - - def test_formatdate_localtime(self): - now = time.time() - self.assertEqual( - utils.parsedate(utils.formatdate(now, localtime=True))[:6], - time.localtime(now)[:6]) - - def test_formatdate_usegmt(self): - now = time.time() - self.assertEqual( - utils.formatdate(now, localtime=False), - time.strftime('%a, %d %b %Y %H:%M:%S -0000', time.gmtime(now))) - self.assertEqual( - utils.formatdate(now, localtime=False, usegmt=True), - time.strftime('%a, %d %b %Y %H:%M:%S GMT', time.gmtime(now))) - - def test_parsedate_none(self): - self.assertEqual(utils.parsedate(''), None) - - def test_parsedate_compact(self): - # The FWS after the comma is optional - self.assertEqual(utils.parsedate('Wed,3 Apr 2002 14:58:26 +0800'), - utils.parsedate('Wed, 3 Apr 2002 14:58:26 +0800')) - - def test_parsedate_no_dayofweek(self): - eq = self.assertEqual - eq(utils.parsedate_tz('25 Feb 2003 13:47:26 -0800'), - (2003, 2, 25, 13, 47, 26, 0, 1, -1, -28800)) - - def test_parsedate_compact_no_dayofweek(self): - eq = self.assertEqual - eq(utils.parsedate_tz('5 Feb 2003 13:47:26 -0800'), - (2003, 2, 5, 13, 47, 26, 0, 1, -1, -28800)) - - def test_parsedate_acceptable_to_time_functions(self): - eq = self.assertEqual - timetup = utils.parsedate('5 Feb 2003 13:47:26 -0800') - t = int(time.mktime(timetup)) - eq(time.localtime(t)[:6], timetup[:6]) - eq(int(time.strftime('%Y', timetup)), 2003) - timetup = utils.parsedate_tz('5 Feb 2003 13:47:26 -0800') - t = int(time.mktime(timetup[:9])) - eq(time.localtime(t)[:6], timetup[:6]) - eq(int(time.strftime('%Y', timetup[:9])), 2003) - - def test_parseaddr_empty(self): - self.assertEqual(utils.parseaddr('<>'), ('', '')) - self.assertEqual(utils.formataddr(utils.parseaddr('<>')), '') - - def test_noquote_dump(self): - self.assertEqual( - utils.formataddr(('A Silly Person', 'person@dom.ain')), - 'A Silly Person ') - - def test_escape_dump(self): - self.assertEqual( - utils.formataddr(('A (Very) Silly Person', 'person@dom.ain')), - r'"A \(Very\) Silly Person" ') - a = r'A \(Special\) Person' - b = 'person@dom.ain' - self.assertEqual(utils.parseaddr(utils.formataddr((a, b))), (a, b)) - - def test_escape_backslashes(self): - self.assertEqual( - utils.formataddr(('Arthur \Backslash\ Foobar', 'person@dom.ain')), - r'"Arthur \\Backslash\\ Foobar" ') - a = r'Arthur \Backslash\ Foobar' - b = 'person@dom.ain' - self.assertEqual(utils.parseaddr(utils.formataddr((a, b))), (a, b)) - - def test_name_with_dot(self): - x = 'John X. Doe ' - y = '"John X. Doe" ' - a, b = ('John X. Doe', 'jxd@example.com') - self.assertEqual(utils.parseaddr(x), (a, b)) - self.assertEqual(utils.parseaddr(y), (a, b)) - # formataddr() quotes the name if there's a dot in it - self.assertEqual(utils.formataddr((a, b)), y) - - def test_quote_dump(self): - self.assertEqual( - utils.formataddr(('A Silly; Person', 'person@dom.ain')), - r'"A Silly; Person" ') - - def test_fix_eols(self): - eq = self.assertEqual - eq(utils.fix_eols('hello'), 'hello') - eq(utils.fix_eols('hello\n'), 'hello\r\n') - eq(utils.fix_eols('hello\r'), 'hello\r\n') - eq(utils.fix_eols('hello\r\n'), 'hello\r\n') - eq(utils.fix_eols('hello\n\r'), 'hello\r\n\r\n') - - def test_charset_richcomparisons(self): - eq = self.assertEqual - ne = self.assertNotEqual - cset1 = Charset() - cset2 = Charset() - eq(cset1, 'us-ascii') - eq(cset1, 'US-ASCII') - eq(cset1, 'Us-AsCiI') - eq('us-ascii', cset1) - eq('US-ASCII', cset1) - eq('Us-AsCiI', cset1) - ne(cset1, 'usascii') - ne(cset1, 'USASCII') - ne(cset1, 'UsAsCiI') - ne('usascii', cset1) - ne('USASCII', cset1) - ne('UsAsCiI', cset1) - eq(cset1, cset2) - eq(cset2, cset1) - - def test_getaddresses(self): - eq = self.assertEqual - eq(utils.getaddresses(['aperson@dom.ain (Al Person)', - 'Bud Person ']), - [('Al Person', 'aperson@dom.ain'), - ('Bud Person', 'bperson@dom.ain')]) - - def test_getaddresses_nasty(self): - eq = self.assertEqual - eq(utils.getaddresses(['foo: ;']), [('', '')]) - eq(utils.getaddresses( - ['[]*-- =~$']), - [('', ''), ('', ''), ('', '*--')]) - eq(utils.getaddresses( - ['foo: ;', '"Jason R. Mastaler" ']), - [('', ''), ('Jason R. Mastaler', 'jason@dom.ain')]) - - def test_getaddresses_embedded_comment(self): - """Test proper handling of a nested comment""" - eq = self.assertEqual - addrs = utils.getaddresses(['User ((nested comment)) ']) - eq(addrs[0][1], 'foo@bar.com') - - def test_utils_quote_unquote(self): - eq = self.assertEqual - msg = Message() - msg.add_header('content-disposition', 'attachment', - filename='foo\\wacky"name') - eq(msg.get_filename(), 'foo\\wacky"name') - - def test_get_body_encoding_with_bogus_charset(self): - charset = Charset('not a charset') - self.assertEqual(charset.get_body_encoding(), 'base64') - - def test_get_body_encoding_with_uppercase_charset(self): - eq = self.assertEqual - msg = Message() - msg['Content-Type'] = 'text/plain; charset=UTF-8' - eq(msg['content-type'], 'text/plain; charset=UTF-8') - charsets = msg.get_charsets() - eq(len(charsets), 1) - eq(charsets[0], 'utf-8') - charset = Charset(charsets[0]) - eq(charset.get_body_encoding(), 'base64') - msg.set_payload('hello world', charset=charset) - eq(msg.get_payload(), 'aGVsbG8gd29ybGQ=\n') - eq(msg.get_payload(decode=True), 'hello world') - eq(msg['content-transfer-encoding'], 'base64') - # Try another one - msg = Message() - msg['Content-Type'] = 'text/plain; charset="US-ASCII"' - charsets = msg.get_charsets() - eq(len(charsets), 1) - eq(charsets[0], 'us-ascii') - charset = Charset(charsets[0]) - eq(charset.get_body_encoding(), encoders.encode_7or8bit) - msg.set_payload('hello world', charset=charset) - eq(msg.get_payload(), 'hello world') - eq(msg['content-transfer-encoding'], '7bit') - - def test_charsets_case_insensitive(self): - lc = Charset('us-ascii') - uc = Charset('US-ASCII') - self.assertEqual(lc.get_body_encoding(), uc.get_body_encoding()) - - def test_partial_falls_inside_message_delivery_status(self): - eq = self.ndiffAssertEqual - # The Parser interface provides chunks of data to FeedParser in 8192 - # byte gulps. SF bug #1076485 found one of those chunks inside - # message/delivery-status header block, which triggered an - # unreadline() of NeedMoreData. - msg = self._msgobj('msg_43.txt') - sfp = StringIO() - iterators._structure(msg, sfp) - eq(sfp.getvalue(), """\ -multipart/report - text/plain - message/delivery-status - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/plain - text/rfc822-headers -""") - - - -# Test the iterator/generators -class TestIterators(TestEmailBase): - def test_body_line_iterator(self): - eq = self.assertEqual - neq = self.ndiffAssertEqual - # First a simple non-multipart message - msg = self._msgobj('msg_01.txt') - it = iterators.body_line_iterator(msg) - lines = list(it) - eq(len(lines), 6) - neq(EMPTYSTRING.join(lines), msg.get_payload()) - # Now a more complicated multipart - msg = self._msgobj('msg_02.txt') - it = iterators.body_line_iterator(msg) - lines = list(it) - eq(len(lines), 43) - fp = openfile('msg_19.txt') - try: - neq(EMPTYSTRING.join(lines), fp.read()) - finally: - fp.close() - - def test_typed_subpart_iterator(self): - eq = self.assertEqual - msg = self._msgobj('msg_04.txt') - it = iterators.typed_subpart_iterator(msg, 'text') - lines = [] - subparts = 0 - for subpart in it: - subparts += 1 - lines.append(subpart.get_payload()) - eq(subparts, 2) - eq(EMPTYSTRING.join(lines), """\ -a simple kind of mirror -to reflect upon our own -a simple kind of mirror -to reflect upon our own -""") - - def test_typed_subpart_iterator_default_type(self): - eq = self.assertEqual - msg = self._msgobj('msg_03.txt') - it = iterators.typed_subpart_iterator(msg, 'text', 'plain') - lines = [] - subparts = 0 - for subpart in it: - subparts += 1 - lines.append(subpart.get_payload()) - eq(subparts, 1) - eq(EMPTYSTRING.join(lines), """\ - -Hi, - -Do you like this message? - --Me -""") - - - -class TestParsers(TestEmailBase): - def test_header_parser(self): - eq = self.assertEqual - # Parse only the headers of a complex multipart MIME document - fp = openfile('msg_02.txt') - try: - msg = HeaderParser().parse(fp) - finally: - fp.close() - eq(msg['from'], 'ppp-request@zzz.org') - eq(msg['to'], 'ppp@zzz.org') - eq(msg.get_content_type(), 'multipart/mixed') - self.assertFalse(msg.is_multipart()) - self.assertTrue(isinstance(msg.get_payload(), str)) - - def test_whitespace_continuation(self): - eq = self.assertEqual - # This message contains a line after the Subject: header that has only - # whitespace, but it is not empty! - msg = bongo.external.email.message_from_string("""\ -From: aperson@dom.ain -To: bperson@dom.ain -Subject: the next line has a space on it -\x20 -Date: Mon, 8 Apr 2002 15:09:19 -0400 -Message-ID: spam - -Here's the message body -""") - eq(msg['subject'], 'the next line has a space on it\n ') - eq(msg['message-id'], 'spam') - eq(msg.get_payload(), "Here's the message body\n") - - def test_whitespace_continuation_last_header(self): - eq = self.assertEqual - # Like the previous test, but the subject line is the last - # header. - msg = bongo.external.email.message_from_string("""\ -From: aperson@dom.ain -To: bperson@dom.ain -Date: Mon, 8 Apr 2002 15:09:19 -0400 -Message-ID: spam -Subject: the next line has a space on it -\x20 - -Here's the message body -""") - eq(msg['subject'], 'the next line has a space on it\n ') - eq(msg['message-id'], 'spam') - eq(msg.get_payload(), "Here's the message body\n") - - def test_crlf_separation(self): - eq = self.assertEqual - fp = openfile('msg_26.txt', mode='rb') - try: - msg = Parser().parse(fp) - finally: - fp.close() - eq(len(msg.get_payload()), 2) - part1 = msg.get_payload(0) - eq(part1.get_content_type(), 'text/plain') - eq(part1.get_payload(), 'Simple email with attachment.\r\n\r\n') - part2 = msg.get_payload(1) - eq(part2.get_content_type(), 'application/riscos') - - def test_multipart_digest_with_extra_mime_headers(self): - eq = self.assertEqual - neq = self.ndiffAssertEqual - fp = openfile('msg_28.txt') - try: - msg = bongo.external.email.message_from_file(fp) - finally: - fp.close() - # Structure is: - # multipart/digest - # message/rfc822 - # text/plain - # message/rfc822 - # text/plain - eq(msg.is_multipart(), 1) - eq(len(msg.get_payload()), 2) - part1 = msg.get_payload(0) - eq(part1.get_content_type(), 'message/rfc822') - eq(part1.is_multipart(), 1) - eq(len(part1.get_payload()), 1) - part1a = part1.get_payload(0) - eq(part1a.is_multipart(), 0) - eq(part1a.get_content_type(), 'text/plain') - neq(part1a.get_payload(), 'message 1\n') - # next message/rfc822 - part2 = msg.get_payload(1) - eq(part2.get_content_type(), 'message/rfc822') - eq(part2.is_multipart(), 1) - eq(len(part2.get_payload()), 1) - part2a = part2.get_payload(0) - eq(part2a.is_multipart(), 0) - eq(part2a.get_content_type(), 'text/plain') - neq(part2a.get_payload(), 'message 2\n') - - def test_three_lines(self): - # A bug report by Andrew McNamara - lines = ['From: Andrew Person From', 'From']) - eq(msg.get_payload(), 'body') - - def test_rfc2822_space_not_allowed_in_header(self): - eq = self.assertEqual - m = '>From foo@example.com 11:25:53\nFrom: bar\n!"#QUX;~: zoo\n\nbody' - msg = bongo.external.email.message_from_string(m) - eq(len(list(msg.keys())), 0) - - def test_rfc2822_one_character_header(self): - eq = self.assertEqual - m = 'A: first header\nB: second header\nCC: third header\n\nbody' - msg = bongo.external.email.message_from_string(m) - headers = list(msg.keys()) - headers.sort() - eq(headers, ['A', 'B', 'CC']) - eq(msg.get_payload(), 'body') - - - -class TestBase64(unittest.TestCase): - def test_len(self): - eq = self.assertEqual - eq(base64mime.base64_len('hello'), - len(base64mime.encode('hello', eol=''))) - for size in range(15): - if size == 0 : bsize = 0 - elif size <= 3 : bsize = 4 - elif size <= 6 : bsize = 8 - elif size <= 9 : bsize = 12 - elif size <= 12: bsize = 16 - else : bsize = 20 - eq(base64mime.base64_len('x'*size), bsize) - - def test_decode(self): - eq = self.assertEqual - eq(base64mime.decode(''), '') - eq(base64mime.decode('aGVsbG8='), 'hello') - eq(base64mime.decode('aGVsbG8=', 'X'), 'hello') - eq(base64mime.decode('aGVsbG8NCndvcmxk\n', 'X'), 'helloXworld') - - def test_encode(self): - eq = self.assertEqual - eq(base64mime.encode(''), '') - eq(base64mime.encode('hello'), 'aGVsbG8=\n') - # Test the binary flag - eq(base64mime.encode('hello\n'), 'aGVsbG8K\n') - eq(base64mime.encode('hello\n', 0), 'aGVsbG8NCg==\n') - # Test the maxlinelen arg - eq(base64mime.encode('xxxx ' * 20, maxlinelen=40), """\ -eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg -eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg -eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg -eHh4eCB4eHh4IA== -""") - # Test the eol argument - eq(base64mime.encode('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\ -eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r -eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r -eHh4eCB4eHh4IHh4eHggeHh4eCB4eHh4IHh4eHgg\r -eHh4eCB4eHh4IA==\r -""") - - def test_header_encode(self): - eq = self.assertEqual - he = base64mime.header_encode - eq(he('hello'), '=?iso-8859-1?b?aGVsbG8=?=') - eq(he('hello\nworld'), '=?iso-8859-1?b?aGVsbG8NCndvcmxk?=') - # Test the charset option - eq(he('hello', charset='iso-8859-2'), '=?iso-8859-2?b?aGVsbG8=?=') - # Test the keep_eols flag - eq(he('hello\nworld', keep_eols=True), - '=?iso-8859-1?b?aGVsbG8Kd29ybGQ=?=') - # Test the maxlinelen argument - eq(he('xxxx ' * 20, maxlinelen=40), """\ -=?iso-8859-1?b?eHh4eCB4eHh4IHh4eHggeHg=?= - =?iso-8859-1?b?eHggeHh4eCB4eHh4IHh4eHg=?= - =?iso-8859-1?b?IHh4eHggeHh4eCB4eHh4IHg=?= - =?iso-8859-1?b?eHh4IHh4eHggeHh4eCB4eHg=?= - =?iso-8859-1?b?eCB4eHh4IHh4eHggeHh4eCA=?= - =?iso-8859-1?b?eHh4eCB4eHh4IHh4eHgg?=""") - # Test the eol argument - eq(he('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\ -=?iso-8859-1?b?eHh4eCB4eHh4IHh4eHggeHg=?=\r - =?iso-8859-1?b?eHggeHh4eCB4eHh4IHh4eHg=?=\r - =?iso-8859-1?b?IHh4eHggeHh4eCB4eHh4IHg=?=\r - =?iso-8859-1?b?eHh4IHh4eHggeHh4eCB4eHg=?=\r - =?iso-8859-1?b?eCB4eHh4IHh4eHggeHh4eCA=?=\r - =?iso-8859-1?b?eHh4eCB4eHh4IHh4eHgg?=""") - - - -class TestQuopri(unittest.TestCase): - def setUp(self): - self.hlit = [chr(x) for x in range(ord('a'), ord('z')+1)] + \ - [chr(x) for x in range(ord('A'), ord('Z')+1)] + \ - [chr(x) for x in range(ord('0'), ord('9')+1)] + \ - ['!', '*', '+', '-', '/', ' '] - self.hnon = [chr(x) for x in range(256) if chr(x) not in self.hlit] - assert len(self.hlit) + len(self.hnon) == 256 - self.blit = [chr(x) for x in range(ord(' '), ord('~')+1)] + ['\t'] - self.blit.remove('=') - self.bnon = [chr(x) for x in range(256) if chr(x) not in self.blit] - assert len(self.blit) + len(self.bnon) == 256 - - def test_header_quopri_check(self): - for c in self.hlit: - self.assertFalse(quoprimime.header_quopri_check(c)) - for c in self.hnon: - self.assertTrue(quoprimime.header_quopri_check(c)) - - def test_body_quopri_check(self): - for c in self.blit: - self.assertFalse(quoprimime.body_quopri_check(c)) - for c in self.bnon: - self.assertTrue(quoprimime.body_quopri_check(c)) - - def test_header_quopri_len(self): - eq = self.assertEqual - hql = quoprimime.header_quopri_len - enc = quoprimime.header_encode - for s in ('hello', 'h@e@l@l@o@'): - # Empty charset and no line-endings. 7 == RFC chrome - eq(hql(s), len(enc(s, charset='', eol=''))-7) - for c in self.hlit: - eq(hql(c), 1) - for c in self.hnon: - eq(hql(c), 3) - - def test_body_quopri_len(self): - eq = self.assertEqual - bql = quoprimime.body_quopri_len - for c in self.blit: - eq(bql(c), 1) - for c in self.bnon: - eq(bql(c), 3) - - def test_quote_unquote_idempotent(self): - for x in range(256): - c = chr(x) - self.assertEqual(quoprimime.unquote(quoprimime.quote(c)), c) - - def test_header_encode(self): - eq = self.assertEqual - he = quoprimime.header_encode - eq(he('hello'), '=?iso-8859-1?q?hello?=') - eq(he('hello\nworld'), '=?iso-8859-1?q?hello=0D=0Aworld?=') - # Test the charset option - eq(he('hello', charset='iso-8859-2'), '=?iso-8859-2?q?hello?=') - # Test the keep_eols flag - eq(he('hello\nworld', keep_eols=True), '=?iso-8859-1?q?hello=0Aworld?=') - # Test a non-ASCII character - eq(he('hello\xc7there'), '=?iso-8859-1?q?hello=C7there?=') - # Test the maxlinelen argument - eq(he('xxxx ' * 20, maxlinelen=40), """\ -=?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xx?= - =?iso-8859-1?q?xx_xxxx_xxxx_xxxx_xxxx?= - =?iso-8859-1?q?_xxxx_xxxx_xxxx_xxxx_x?= - =?iso-8859-1?q?xxx_xxxx_xxxx_xxxx_xxx?= - =?iso-8859-1?q?x_xxxx_xxxx_?=""") - # Test the eol argument - eq(he('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\ -=?iso-8859-1?q?xxxx_xxxx_xxxx_xxxx_xx?=\r - =?iso-8859-1?q?xx_xxxx_xxxx_xxxx_xxxx?=\r - =?iso-8859-1?q?_xxxx_xxxx_xxxx_xxxx_x?=\r - =?iso-8859-1?q?xxx_xxxx_xxxx_xxxx_xxx?=\r - =?iso-8859-1?q?x_xxxx_xxxx_?=""") - - def test_decode(self): - eq = self.assertEqual - eq(quoprimime.decode(''), '') - eq(quoprimime.decode('hello'), 'hello') - eq(quoprimime.decode('hello', 'X'), 'hello') - eq(quoprimime.decode('hello\nworld', 'X'), 'helloXworld') - - def test_encode(self): - eq = self.assertEqual - eq(quoprimime.encode(''), '') - eq(quoprimime.encode('hello'), 'hello') - # Test the binary flag - eq(quoprimime.encode('hello\r\nworld'), 'hello\nworld') - eq(quoprimime.encode('hello\r\nworld', 0), 'hello\nworld') - # Test the maxlinelen arg - eq(quoprimime.encode('xxxx ' * 20, maxlinelen=40), """\ -xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx= - xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx= -x xxxx xxxx xxxx xxxx=20""") - # Test the eol argument - eq(quoprimime.encode('xxxx ' * 20, maxlinelen=40, eol='\r\n'), """\ -xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxxx=\r - xxxx xxxx xxxx xxxx xxxx xxxx xxxx xxx=\r -x xxxx xxxx xxxx xxxx=20""") - eq(quoprimime.encode("""\ -one line - -two line"""), """\ -one line - -two line""") - - - -# Test the Charset class -class TestCharset(unittest.TestCase): - def tearDown(self): - from bongo.external.email import charset as CharsetModule - try: - del CharsetModule.CHARSETS['fake'] - except KeyError: - pass - - def test_idempotent(self): - eq = self.assertEqual - # Make sure us-ascii = no Unicode conversion - c = Charset('us-ascii') - s = 'Hello World!' - sp = c.to_splittable(s) - eq(s, c.from_splittable(sp)) - # test 8-bit idempotency with us-ascii - s = '\xa4\xa2\xa4\xa4\xa4\xa6\xa4\xa8\xa4\xaa' - sp = c.to_splittable(s) - eq(s, c.from_splittable(sp)) - - def test_body_encode(self): - eq = self.assertEqual - # Try a charset with QP body encoding - c = Charset('iso-8859-1') - eq('hello w=F6rld', c.body_encode('hello w\xf6rld')) - # Try a charset with Base64 body encoding - c = Charset('utf-8') - eq('aGVsbG8gd29ybGQ=\n', c.body_encode('hello world')) - # Try a charset with None body encoding - c = Charset('us-ascii') - eq('hello world', c.body_encode('hello world')) - # Try the convert argument, where input codec <> output codec - c = Charset('euc-jp') - # With apologies to Tokio Kikuchi ;) - try: - eq('\x1b$B5FCO;~IW\x1b(B', - c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7')) - eq('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7', - c.body_encode('\xb5\xc6\xc3\xcf\xbb\xfe\xc9\xd7', False)) - except LookupError: - # We probably don't have the Japanese codecs installed - pass - # Testing SF bug #625509, which we have to fake, since there are no - # built-in encodings where the header encoding is QP but the body - # encoding is not. - from bongo.external.email import charset as CharsetModule - CharsetModule.add_charset('fake', CharsetModule.QP, None) - c = Charset('fake') - eq('hello w\xf6rld', c.body_encode('hello w\xf6rld')) - - def test_unicode_charset_name(self): - charset = Charset('us-ascii') - self.assertEqual(str(charset), 'us-ascii') - self.assertRaises(errors.CharsetError, Charset, 'asc\xffii') - - - -# Test multilingual MIME headers. -class TestHeader(TestEmailBase): - def test_simple(self): - eq = self.ndiffAssertEqual - h = Header('Hello World!') - eq(h.encode(), 'Hello World!') - h.append(' Goodbye World!') - eq(h.encode(), 'Hello World! Goodbye World!') - - def test_simple_surprise(self): - eq = self.ndiffAssertEqual - h = Header('Hello World!') - eq(h.encode(), 'Hello World!') - h.append('Goodbye World!') - eq(h.encode(), 'Hello World! Goodbye World!') - - def test_header_needs_no_decoding(self): - h = 'no decoding needed' - self.assertEqual(decode_header(h), [(h, None)]) - - def test_long(self): - h = Header("I am the very model of a modern Major-General; I've information vegetable, animal, and mineral; I know the kings of England, and I quote the fights historical from Marathon to Waterloo, in order categorical; I'm very well acquainted, too, with matters mathematical; I understand equations, both the simple and quadratical; about binomial theorem I'm teeming with a lot o' news, with many cheerful facts about the square of the hypotenuse.", - maxlinelen=76) - for l in h.encode(splitchars=' ').split('\n '): - self.assertTrue(len(l) <= 76) - - def test_multilingual(self): - eq = self.ndiffAssertEqual - g = Charset("iso-8859-1") - cz = Charset("iso-8859-2") - utf8 = Charset("utf-8") - g_head = "Die Mieter treten hier ein werden mit einem Foerderband komfortabel den Korridor entlang, an s\xfcdl\xfcndischen Wandgem\xe4lden vorbei, gegen die rotierenden Klingen bef\xf6rdert. " - cz_head = "Finan\xe8ni metropole se hroutily pod tlakem jejich d\xf9vtipu.. " - utf8_head = "\u6b63\u78ba\u306b\u8a00\u3046\u3068\u7ffb\u8a33\u306f\u3055\u308c\u3066\u3044\u307e\u305b\u3093\u3002\u4e00\u90e8\u306f\u30c9\u30a4\u30c4\u8a9e\u3067\u3059\u304c\u3001\u3042\u3068\u306f\u3067\u305f\u3089\u3081\u3067\u3059\u3002\u5b9f\u969b\u306b\u306f\u300cWenn ist das Nunstuck git und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt gersput.\u300d\u3068\u8a00\u3063\u3066\u3044\u307e\u3059\u3002".encode("utf-8") - h = Header(g_head, g) - h.append(cz_head, cz) - h.append(utf8_head, utf8) - enc = h.encode() - eq(enc, """\ -=?iso-8859-1?q?Die_Mieter_treten_hier_ein_werden_mit_einem_Foerderband_ko?= - =?iso-8859-1?q?mfortabel_den_Korridor_entlang=2C_an_s=FCdl=FCndischen_Wan?= - =?iso-8859-1?q?dgem=E4lden_vorbei=2C_gegen_die_rotierenden_Klingen_bef=F6?= - =?iso-8859-1?q?rdert=2E_?= =?iso-8859-2?q?Finan=E8ni_metropole_se_hroutily?= - =?iso-8859-2?q?_pod_tlakem_jejich_d=F9vtipu=2E=2E_?= =?utf-8?b?5q2j56K6?= - =?utf-8?b?44Gr6KiA44GG44Go57+76Kiz44Gv44GV44KM44Gm44GE44G+44Gb44KT44CC?= - =?utf-8?b?5LiA6YOo44Gv44OJ44Kk44OE6Kqe44Gn44GZ44GM44CB44GC44Go44Gv44Gn?= - =?utf-8?b?44Gf44KJ44KB44Gn44GZ44CC5a6f6Zqb44Gr44Gv44CMV2VubiBpc3QgZGFz?= - =?utf-8?q?_Nunstuck_git_und_Slotermeyer=3F_Ja!_Beiherhund_das_Oder_die_Fl?= - =?utf-8?b?aXBwZXJ3YWxkdCBnZXJzcHV0LuOAjeOBqOiogOOBo+OBpuOBhOOBvuOBmQ==?= - =?utf-8?b?44CC?=""") - eq(decode_header(enc), - [(g_head, "iso-8859-1"), (cz_head, "iso-8859-2"), - (utf8_head, "utf-8")]) - ustr = str(h) - eq(ustr.encode('utf-8'), - 'Die Mieter treten hier ein werden mit einem Foerderband ' - 'komfortabel den Korridor entlang, an s\xc3\xbcdl\xc3\xbcndischen ' - 'Wandgem\xc3\xa4lden vorbei, gegen die rotierenden Klingen ' - 'bef\xc3\xb6rdert. Finan\xc4\x8dni metropole se hroutily pod ' - 'tlakem jejich d\xc5\xafvtipu.. \xe6\xad\xa3\xe7\xa2\xba\xe3\x81' - '\xab\xe8\xa8\x80\xe3\x81\x86\xe3\x81\xa8\xe7\xbf\xbb\xe8\xa8\xb3' - '\xe3\x81\xaf\xe3\x81\x95\xe3\x82\x8c\xe3\x81\xa6\xe3\x81\x84\xe3' - '\x81\xbe\xe3\x81\x9b\xe3\x82\x93\xe3\x80\x82\xe4\xb8\x80\xe9\x83' - '\xa8\xe3\x81\xaf\xe3\x83\x89\xe3\x82\xa4\xe3\x83\x84\xe8\xaa\x9e' - '\xe3\x81\xa7\xe3\x81\x99\xe3\x81\x8c\xe3\x80\x81\xe3\x81\x82\xe3' - '\x81\xa8\xe3\x81\xaf\xe3\x81\xa7\xe3\x81\x9f\xe3\x82\x89\xe3\x82' - '\x81\xe3\x81\xa7\xe3\x81\x99\xe3\x80\x82\xe5\xae\x9f\xe9\x9a\x9b' - '\xe3\x81\xab\xe3\x81\xaf\xe3\x80\x8cWenn ist das Nunstuck git ' - 'und Slotermeyer? Ja! Beiherhund das Oder die Flipperwaldt ' - 'gersput.\xe3\x80\x8d\xe3\x81\xa8\xe8\xa8\x80\xe3\x81\xa3\xe3\x81' - '\xa6\xe3\x81\x84\xe3\x81\xbe\xe3\x81\x99\xe3\x80\x82') - # Test make_header() - newh = make_header(decode_header(enc)) - eq(newh, enc) - - def test_header_ctor_default_args(self): - eq = self.ndiffAssertEqual - h = Header() - eq(h, '') - h.append('foo', Charset('iso-8859-1')) - eq(h, '=?iso-8859-1?q?foo?=') - - def test_explicit_maxlinelen(self): - eq = self.ndiffAssertEqual - hstr = 'A very long line that must get split to something other than at the 76th character boundary to test the non-default behavior' - h = Header(hstr) - eq(h.encode(), '''\ -A very long line that must get split to something other than at the 76th - character boundary to test the non-default behavior''') - h = Header(hstr, header_name='Subject') - eq(h.encode(), '''\ -A very long line that must get split to something other than at the - 76th character boundary to test the non-default behavior''') - h = Header(hstr, maxlinelen=1024, header_name='Subject') - eq(h.encode(), hstr) - - def test_us_ascii_header(self): - eq = self.assertEqual - s = 'hello' - x = decode_header(s) - eq(x, [('hello', None)]) - h = make_header(x) - eq(s, h.encode()) - - def test_string_charset(self): - eq = self.assertEqual - h = Header() - h.append('hello', 'iso-8859-1') - eq(h, '=?iso-8859-1?q?hello?=') - -## def test_unicode_error(self): -## raises = self.assertRaises -## raises(UnicodeError, Header, u'[P\xf6stal]', 'us-ascii') -## raises(UnicodeError, Header, '[P\xf6stal]', 'us-ascii') -## h = Header() -## raises(UnicodeError, h.append, u'[P\xf6stal]', 'us-ascii') -## raises(UnicodeError, h.append, '[P\xf6stal]', 'us-ascii') -## raises(UnicodeError, Header, u'\u83ca\u5730\u6642\u592b', 'iso-8859-1') - - def test_utf8_shortest(self): - eq = self.assertEqual - h = Header('p\xf6stal', 'utf-8') - eq(h.encode(), '=?utf-8?q?p=C3=B6stal?=') - h = Header('\u83ca\u5730\u6642\u592b', 'utf-8') - eq(h.encode(), '=?utf-8?b?6I+K5Zyw5pmC5aSr?=') - - def test_bad_8bit_header(self): - raises = self.assertRaises - eq = self.assertEqual - x = 'Ynwp4dUEbay Auction Semiar- No Charge \x96 Earn Big' - raises(UnicodeError, Header, x) - h = Header() - raises(UnicodeError, h.append, x) - eq(str(Header(x, errors='replace')), x) - h.append(x, errors='replace') - eq(str(h), x) - - def test_encoded_adjacent_nonencoded(self): - eq = self.assertEqual - h = Header() - h.append('hello', 'iso-8859-1') - h.append('world') - s = h.encode() - eq(s, '=?iso-8859-1?q?hello?= world') - h = make_header(decode_header(s)) - eq(h.encode(), s) - - def test_whitespace_eater(self): - eq = self.assertEqual - s = 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztk=?= =?koi8-r?q?=CA?= zz.' - parts = decode_header(s) - eq(parts, [('Subject:', None), ('\xf0\xd2\xcf\xd7\xc5\xd2\xcb\xc1 \xce\xc1 \xc6\xc9\xce\xc1\xcc\xd8\xce\xd9\xca', 'koi8-r'), ('zz.', None)]) - hdr = make_header(parts) - eq(hdr.encode(), - 'Subject: =?koi8-r?b?8NLP18XSy8EgzsEgxsnOwczYztnK?= zz.') - - def test_broken_base64_header(self): - raises = self.assertRaises - s = 'Subject: =?EUC-KR?B?CSixpLDtKSC/7Liuvsax4iC6uLmwMcijIKHaILzSwd/H0SC8+LCjwLsgv7W/+Mj3IQ?=' - raises(errors.HeaderParseError, decode_header, s) - - - -# Test RFC 2231 header parameters (en/de)coding -class TestRFC2231(TestEmailBase): - def test_get_param(self): - eq = self.assertEqual - msg = self._msgobj('msg_29.txt') - eq(msg.get_param('title'), - ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!')) - eq(msg.get_param('title', unquote=False), - ('us-ascii', 'en', '"This is even more ***fun*** isn\'t it!"')) - - def test_set_param(self): - eq = self.assertEqual - msg = Message() - msg.set_param('title', 'This is even more ***fun*** isn\'t it!', - charset='us-ascii') - eq(msg.get_param('title'), - ('us-ascii', '', 'This is even more ***fun*** isn\'t it!')) - msg.set_param('title', 'This is even more ***fun*** isn\'t it!', - charset='us-ascii', language='en') - eq(msg.get_param('title'), - ('us-ascii', 'en', 'This is even more ***fun*** isn\'t it!')) - msg = self._msgobj('msg_01.txt') - msg.set_param('title', 'This is even more ***fun*** isn\'t it!', - charset='us-ascii', language='en') - eq(msg.as_string(), """\ -Return-Path: -Delivered-To: bbb@zzz.org -Received: by mail.zzz.org (Postfix, from userid 889) -\tid 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT) -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit -Message-ID: <15090.61304.110929.45684@aaa.zzz.org> -From: bbb@ddd.com (John X. Doe) -To: bbb@zzz.org -Subject: This is a test message -Date: Fri, 4 May 2001 14:05:44 -0400 -Content-Type: text/plain; charset=us-ascii; -\ttitle*="us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21" - - -Hi, - -Do you like this message? - --Me -""") - - def test_del_param(self): - eq = self.ndiffAssertEqual - msg = self._msgobj('msg_01.txt') - msg.set_param('foo', 'bar', charset='us-ascii', language='en') - msg.set_param('title', 'This is even more ***fun*** isn\'t it!', - charset='us-ascii', language='en') - msg.del_param('foo', header='Content-Type') - eq(msg.as_string(), """\ -Return-Path: -Delivered-To: bbb@zzz.org -Received: by mail.zzz.org (Postfix, from userid 889) -\tid 27CEAD38CC; Fri, 4 May 2001 14:05:44 -0400 (EDT) -MIME-Version: 1.0 -Content-Transfer-Encoding: 7bit -Message-ID: <15090.61304.110929.45684@aaa.zzz.org> -From: bbb@ddd.com (John X. Doe) -To: bbb@zzz.org -Subject: This is a test message -Date: Fri, 4 May 2001 14:05:44 -0400 -Content-Type: text/plain; charset="us-ascii"; -\ttitle*="us-ascii'en'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20isn%27t%20it%21" - - -Hi, - -Do you like this message? - --Me -""") - - def test_rfc2231_get_content_charset(self): - eq = self.assertEqual - msg = self._msgobj('msg_32.txt') - eq(msg.get_content_charset(), 'us-ascii') - - def test_rfc2231_no_language_or_charset(self): - m = '''\ -Content-Transfer-Encoding: 8bit -Content-Disposition: inline; filename="file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm" -Content-Type: text/html; NAME*0=file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEM; NAME*1=P_nsmail.htm - -''' - msg = bongo.external.email.message_from_string(m) - param = msg.get_param('NAME') - self.assertFalse(isinstance(param, tuple)) - self.assertEqual( - param, - 'file____C__DOCUMENTS_20AND_20SETTINGS_FABIEN_LOCAL_20SETTINGS_TEMP_nsmail.htm') - - def test_rfc2231_no_language_or_charset_in_filename(self): - m = '''\ -Content-Disposition: inline; -\tfilename*0*="''This%20is%20even%20more%20"; -\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20"; -\tfilename*2="is it not.pdf" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual(msg.get_filename(), - 'This is even more ***fun*** is it not.pdf') - - def test_rfc2231_no_language_or_charset_in_filename_encoded(self): - m = '''\ -Content-Disposition: inline; -\tfilename*0*="''This%20is%20even%20more%20"; -\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20"; -\tfilename*2="is it not.pdf" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual(msg.get_filename(), - 'This is even more ***fun*** is it not.pdf') - - def test_rfc2231_partly_encoded(self): - m = '''\ -Content-Disposition: inline; -\tfilename*0="''This%20is%20even%20more%20"; -\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20"; -\tfilename*2="is it not.pdf" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual( - msg.get_filename(), - 'This%20is%20even%20more%20***fun*** is it not.pdf') - - def test_rfc2231_partly_nonencoded(self): - m = '''\ -Content-Disposition: inline; -\tfilename*0="This%20is%20even%20more%20"; -\tfilename*1="%2A%2A%2Afun%2A%2A%2A%20"; -\tfilename*2="is it not.pdf" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual( - msg.get_filename(), - 'This%20is%20even%20more%20%2A%2A%2Afun%2A%2A%2A%20is it not.pdf') - - def test_rfc2231_no_language_or_charset_in_boundary(self): - m = '''\ -Content-Type: multipart/alternative; -\tboundary*0*="''This%20is%20even%20more%20"; -\tboundary*1*="%2A%2A%2Afun%2A%2A%2A%20"; -\tboundary*2="is it not.pdf" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual(msg.get_boundary(), - 'This is even more ***fun*** is it not.pdf') - - def test_rfc2231_no_language_or_charset_in_charset(self): - # This is a nonsensical charset value, but tests the code anyway - m = '''\ -Content-Type: text/plain; -\tcharset*0*="This%20is%20even%20more%20"; -\tcharset*1*="%2A%2A%2Afun%2A%2A%2A%20"; -\tcharset*2="is it not.pdf" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual(msg.get_content_charset(), - 'this is even more ***fun*** is it not.pdf') - - def test_rfc2231_bad_encoding_in_filename(self): - m = '''\ -Content-Disposition: inline; -\tfilename*0*="bogus'xx'This%20is%20even%20more%20"; -\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20"; -\tfilename*2="is it not.pdf" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual(msg.get_filename(), - 'This is even more ***fun*** is it not.pdf') - - def test_rfc2231_bad_encoding_in_charset(self): - m = """\ -Content-Type: text/plain; charset*=bogus''utf-8%E2%80%9D - -""" - msg = bongo.external.email.message_from_string(m) - # This should return None because non-ascii characters in the charset - # are not allowed. - self.assertEqual(msg.get_content_charset(), None) - - def test_rfc2231_bad_character_in_charset(self): - m = """\ -Content-Type: text/plain; charset*=ascii''utf-8%E2%80%9D - -""" - msg = bongo.external.email.message_from_string(m) - # This should return None because non-ascii characters in the charset - # are not allowed. - self.assertEqual(msg.get_content_charset(), None) - - def test_rfc2231_bad_character_in_filename(self): - m = '''\ -Content-Disposition: inline; -\tfilename*0*="ascii'xx'This%20is%20even%20more%20"; -\tfilename*1*="%2A%2A%2Afun%2A%2A%2A%20"; -\tfilename*2*="is it not.pdf%E2" - -''' - msg = bongo.external.email.message_from_string(m) - self.assertEqual(msg.get_filename(), - 'This is even more ***fun*** is it not.pdf\ufffd') - - def test_rfc2231_unknown_encoding(self): - m = """\ -Content-Transfer-Encoding: 8bit -Content-Disposition: inline; filename*=X-UNKNOWN''myfile.txt - -""" - msg = bongo.external.email.message_from_string(m) - self.assertEqual(msg.get_filename(), 'myfile.txt') - - def test_rfc2231_single_tick_in_filename_extended(self): - eq = self.assertEqual - m = """\ -Content-Type: application/x-foo; -\tname*0*=\"Frank's\"; name*1*=\" Document\" - -""" - msg = bongo.external.email.message_from_string(m) - charset, language, s = msg.get_param('name') - eq(charset, None) - eq(language, None) - eq(s, "Frank's Document") - - def test_rfc2231_single_tick_in_filename(self): - m = """\ -Content-Type: application/x-foo; name*0=\"Frank's\"; name*1=\" Document\" - -""" - msg = bongo.external.email.message_from_string(m) - param = msg.get_param('name') - self.assertFalse(isinstance(param, tuple)) - self.assertEqual(param, "Frank's Document") - - def test_rfc2231_tick_attack_extended(self): - eq = self.assertEqual - m = """\ -Content-Type: application/x-foo; -\tname*0*=\"us-ascii'en-us'Frank's\"; name*1*=\" Document\" - -""" - msg = bongo.external.email.message_from_string(m) - charset, language, s = msg.get_param('name') - eq(charset, 'us-ascii') - eq(language, 'en-us') - eq(s, "Frank's Document") - - def test_rfc2231_tick_attack(self): - m = """\ -Content-Type: application/x-foo; -\tname*0=\"us-ascii'en-us'Frank's\"; name*1=\" Document\" - -""" - msg = bongo.external.email.message_from_string(m) - param = msg.get_param('name') - self.assertFalse(isinstance(param, tuple)) - self.assertEqual(param, "us-ascii'en-us'Frank's Document") - - def test_rfc2231_no_extended_values(self): - eq = self.assertEqual - m = """\ -Content-Type: application/x-foo; name=\"Frank's Document\" - -""" - msg = bongo.external.email.message_from_string(m) - eq(msg.get_param('name'), "Frank's Document") - - def test_rfc2231_encoded_then_unencoded_segments(self): - eq = self.assertEqual - m = """\ -Content-Type: application/x-foo; -\tname*0*=\"us-ascii'en-us'My\"; -\tname*1=\" Document\"; -\tname*2*=\" For You\" - -""" - msg = bongo.external.email.message_from_string(m) - charset, language, s = msg.get_param('name') - eq(charset, 'us-ascii') - eq(language, 'en-us') - eq(s, 'My Document For You') - - def test_rfc2231_unencoded_then_encoded_segments(self): - eq = self.assertEqual - m = """\ -Content-Type: application/x-foo; -\tname*0=\"us-ascii'en-us'My\"; -\tname*1*=\" Document\"; -\tname*2*=\" For You\" - -""" - msg = bongo.external.email.message_from_string(m) - charset, language, s = msg.get_param('name') - eq(charset, 'us-ascii') - eq(language, 'en-us') - eq(s, 'My Document For You') - - - -def _testclasses(): - mod = sys.modules[__name__] - return [getattr(mod, name) for name in dir(mod) if name.startswith('Test')] - - -def suite(): - suite = unittest.TestSuite() - for testclass in _testclasses(): - suite.addTest(unittest.makeSuite(testclass)) - return suite - - -def test_main(): - for testclass in _testclasses(): - run_unittest(testclass) - - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/src/libs/python/bongo/external/email/test/test_email_torture.py b/src/libs/python/bongo/external/email/test/test_email_torture.py deleted file mode 100644 index 472a68c..0000000 --- a/src/libs/python/bongo/external/email/test/test_email_torture.py +++ /dev/null @@ -1,138 +0,0 @@ -# Copyright (C) 2002-2004 Python Software Foundation -# -# A torture test of the email package. This should not be run as part of the -# standard Python test suite since it requires several meg of email messages -# collected in the wild. These source messages are not checked into the -# Python distro, but are available as part of the standalone email package at -# http://sf.net/projects/mimelib - -import sys -import os -import unittest -from io import StringIO -from types import ListType - -from bongo.external.email.test.test_email import TestEmailBase -from test.test_support import TestSkipped - -import bongo -import bongo.external -import bongo.external.email -from bongo.external.email import __file__ as testfile -from bongo.external.email.Iterators import _structure -from bongo.external.email import message_from_file -def openfile(filename): - from os.path import join, dirname, abspath - path = abspath(join(dirname(testfile), os.pardir, 'moredata', filename)) - return open(path, 'r') - -# Prevent this test from running in the Python distro -try: - openfile('crispin-torture.txt') -except IOError: - raise TestSkipped - - - -class TortureBase(TestEmailBase): - def _msgobj(self, filename): - fp = openfile(filename) - try: - msg = message_from_file(fp) - finally: - fp.close() - return msg - - - -class TestCrispinTorture(TortureBase): - # Mark Crispin's torture test from the SquirrelMail project - def test_mondo_message(self): - eq = self.assertEqual - neq = self.ndiffAssertEqual - msg = self._msgobj('crispin-torture.txt') - payload = msg.get_payload() - eq(type(payload), ListType) - eq(len(payload), 12) - eq(msg.preamble, None) - eq(msg.epilogue, '\n') - # Probably the best way to verify the message is parsed correctly is to - # dump its structure and compare it against the known structure. - fp = StringIO() - _structure(msg, fp=fp) - neq(fp.getvalue(), """\ -multipart/mixed - text/plain - message/rfc822 - multipart/alternative - text/plain - multipart/mixed - text/richtext - application/andrew-inset - message/rfc822 - audio/basic - audio/basic - image/pbm - message/rfc822 - multipart/mixed - multipart/mixed - text/plain - audio/x-sun - multipart/mixed - image/gif - image/gif - application/x-be2 - application/atomicmail - audio/x-sun - message/rfc822 - multipart/mixed - text/plain - image/pgm - text/plain - message/rfc822 - multipart/mixed - text/plain - image/pbm - message/rfc822 - application/postscript - image/gif - message/rfc822 - multipart/mixed - audio/basic - audio/basic - message/rfc822 - multipart/mixed - application/postscript - text/plain - message/rfc822 - multipart/mixed - text/plain - multipart/parallel - image/gif - audio/basic - application/atomicmail - message/rfc822 - audio/x-sun -""") - - -def _testclasses(): - mod = sys.modules[__name__] - return [getattr(mod, name) for name in dir(mod) if name.startswith('Test')] - - -def suite(): - suite = unittest.TestSuite() - for testclass in _testclasses(): - suite.addTest(unittest.makeSuite(testclass)) - return suite - - -def test_main(): - for testclass in _testclasses(): - test_support.run_unittest(testclass) - - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/src/libs/python/bongo/external/email/utils.py b/src/libs/python/bongo/external/email/utils.py deleted file mode 100644 index f9b661c..0000000 --- a/src/libs/python/bongo/external/email/utils.py +++ /dev/null @@ -1,327 +0,0 @@ -# Copyright (C) 2001-2006 Python Software Foundation -# Author: Barry Warsaw -# Contact: email-sig@python.org - -"""Miscellaneous utilities.""" - -__all__ = [ - 'collapse_rfc2231_value', - 'decode_params', - 'decode_rfc2231', - 'encode_rfc2231', - 'formataddr', - 'formatdate', - 'getaddresses', - 'make_msgid', - 'parseaddr', - 'parsedate', - 'parsedate_tz', - 'unquote', - ] - -import os -import re -import time -import base64 -import random -import socket -import urllib.request, urllib.parse, urllib.error -import warnings -from io import StringIO - -from bongo.external.email._parseaddr import quote -from bongo.external.email._parseaddr import AddressList as _AddressList -from bongo.external.email._parseaddr import mktime_tz - -# We need wormarounds for bugs in these methods in older Pythons (see below) -from bongo.external.email._parseaddr import parsedate as _parsedate -from bongo.external.email._parseaddr import parsedate_tz as _parsedate_tz - -from quopri import decodestring as _qdecode - -# Intrapackage imports -from bongo.external.email.encoders import _bencode, _qencode - -COMMASPACE = ', ' -EMPTYSTRING = '' -UEMPTYSTRING = '' -CRLF = '\r\n' -TICK = "'" - -specialsre = re.compile(r'[][\\()<>@,:;".]') -escapesre = re.compile(r'[][\\()"]') - - - -# Helpers - -def _identity(s): - return s - - -def _bdecode(s): - # We can't quite use base64.encodestring() since it tacks on a "courtesy - # newline". Blech! - if not s: - return s - value = base64.decodestring(s) - if not s.endswith('\n') and value.endswith('\n'): - return value[:-1] - return value - - - -def fix_eols(s): - """Replace all line-ending characters with \r\n.""" - # Fix newlines with no preceding carriage return - s = re.sub(r'(?', name) - return '%s%s%s <%s>' % (quotes, name, quotes, address) - return address - - - -def getaddresses(fieldvalues): - """Return a list of (REALNAME, EMAIL) for each fieldvalue.""" - all = COMMASPACE.join(fieldvalues) - a = _AddressList(all) - return a.addresslist - - - -ecre = re.compile(r''' - =\? # literal =? - (?P[^?]*?) # non-greedy up to the next ? is the charset - \? # literal ? - (?P[qb]) # either a "q" or a "b", case insensitive - \? # literal ? - (?P.*?) # non-greedy up to the next ?= is the atom - \?= # literal ?= - ''', re.VERBOSE | re.IGNORECASE) - - - -def formatdate(timeval=None, localtime=False, usegmt=False): - """Returns a date string as specified by RFC 2822, e.g.: - - Fri, 09 Nov 2001 01:08:47 -0000 - - Optional timeval if given is a floating point time value as accepted by - gmtime() and localtime(), otherwise the current time is used. - - Optional localtime is a flag that when True, interprets timeval, and - returns a date relative to the local timezone instead of UTC, properly - taking daylight savings time into account. - - Optional argument usegmt means that the timezone is written out as - an ascii string, not numeric one (so "GMT" instead of "+0000"). This - is needed for HTTP, and is only used when localtime==False. - """ - # Note: we cannot use strftime() because that honors the locale and RFC - # 2822 requires that day and month names be the English abbreviations. - if timeval is None: - timeval = time.time() - if localtime: - now = time.localtime(timeval) - # Calculate timezone offset, based on whether the local zone has - # daylight savings time, and whether DST is in effect. - if time.daylight and now[-1]: - offset = time.altzone - else: - offset = time.timezone - hours, minutes = divmod(abs(offset), 3600) - # Remember offset is in seconds west of UTC, but the timezone is in - # minutes east of UTC, so the signs differ. - if offset > 0: - sign = '-' - else: - sign = '+' - zone = '%s%02d%02d' % (sign, hours, minutes // 60) - else: - now = time.gmtime(timeval) - # Timezone offset is always -0000 - if usegmt: - zone = 'GMT' - else: - zone = '-0000' - return '%s, %02d %s %04d %02d:%02d:%02d %s' % ( - ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'][now[6]], - now[2], - ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', - 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][now[1] - 1], - now[0], now[3], now[4], now[5], - zone) - - - -def make_msgid(idstring=None): - """Returns a string suitable for RFC 2822 compliant Message-ID, e.g: - - <20020201195627.33539.96671@nightshade.la.mastaler.com> - - Optional idstring if given is a string used to strengthen the - uniqueness of the message id. - """ - timeval = time.time() - utcdate = time.strftime('%Y%m%d%H%M%S', time.gmtime(timeval)) - pid = os.getpid() - randint = random.randrange(100000) - if idstring is None: - idstring = '' - else: - idstring = '.' + idstring - idhost = socket.getfqdn() - msgid = '<%s.%s.%s%s@%s>' % (utcdate, pid, randint, idstring, idhost) - return msgid - - - -# These functions are in the standalone mimelib version only because they've -# subsequently been fixed in the latest Python versions. We use this to worm -# around broken older Pythons. -def parsedate(data): - if not data: - return None - return _parsedate(data) - - -def parsedate_tz(data): - if not data: - return None - return _parsedate_tz(data) - - -def parseaddr(addr): - addrs = _AddressList(addr).addresslist - if not addrs: - return '', '' - return addrs[0] - - -# rfc822.unquote() doesn't properly de-backslash-ify in Python pre-2.3. -def unquote(str): - """Remove quotes from a string.""" - if len(str) > 1: - if str.startswith('"') and str.endswith('"'): - return str[1:-1].replace('\\\\', '\\').replace('\\"', '"') - if str.startswith('<') and str.endswith('>'): - return str[1:-1] - return str - - - -# RFC2231-related functions - parameter encoding and decoding -def decode_rfc2231(s): - """Decode string according to RFC 2231""" - parts = s.split(TICK, 2) - if len(parts) <= 2: - return None, None, s - if len(parts) > 3: - charset, language = parts[:2] - s = TICK.join(parts[2:]) - return charset, language, s - return parts - - -def encode_rfc2231(s, charset=None, language=None): - """Encode string according to RFC 2231. - - If neither charset nor language is given, then s is returned as-is. If - charset is given but not language, the string is encoded using the empty - string for language. - """ - import urllib.request, urllib.parse, urllib.error - s = urllib.parse.quote(s, safe='') - if charset is None and language is None: - return s - if language is None: - language = '' - return "%s'%s'%s" % (charset, language, s) - - -rfc2231_continuation = re.compile(r'^(?P\w+)\*((?P[0-9]+)\*?)?$') - -def decode_params(params): - """Decode parameters list according to RFC 2231. - - params is a sequence of 2-tuples containing (param name, string value). - """ - # Copy params so we don't mess with the original - params = params[:] - new_params = [] - # Map parameter's name to a list of continuations. The values are a - # 3-tuple of the continuation number, the string value, and a flag - # specifying whether a particular segment is %-encoded. - rfc2231_params = {} - name, value = params.pop(0) - new_params.append((name, value)) - while params: - name, value = params.pop(0) - if name.endswith('*'): - encoded = True - else: - encoded = False - value = unquote(value) - mo = rfc2231_continuation.match(name) - if mo: - name, num = mo.group('name', 'num') - if num is not None: - num = int(num) - rfc2231_params.setdefault(name, []).append((num, value, encoded)) - else: - new_params.append((name, '"%s"' % quote(value))) - if rfc2231_params: - for name, continuations in list(rfc2231_params.items()): - value = [] - extended = False - # Sort by number - continuations.sort() - # And now append all values in numerical order, converting - # %-encodings for the encoded segments. If any of the - # continuation names ends in a *, then the entire string, after - # decoding segments and concatenating, must have the charset and - # language specifiers at the beginning of the string. - for num, s, encoded in continuations: - if encoded: - s = urllib.parse.unquote(s) - extended = True - value.append(s) - value = quote(EMPTYSTRING.join(value)) - if extended: - charset, language, value = decode_rfc2231(value) - new_params.append((name, (charset, language, '"%s"' % value))) - else: - new_params.append((name, '"%s"' % value)) - return new_params - -def collapse_rfc2231_value(value, errors='replace', - fallback_charset='us-ascii'): - if isinstance(value, tuple): - rawval = unquote(value[2]) - charset = value[0] or 'us-ascii' - try: - return str(rawval, charset, errors) - except LookupError: - # XXX charset is unknown to Python. - return str(rawval, fallback_charset, errors) - else: - return unquote(value) diff --git a/src/libs/python/bongo/external/simplejson/__init__.py b/src/libs/python/bongo/external/simplejson/__init__.py deleted file mode 100644 index 63b2e24..0000000 --- a/src/libs/python/bongo/external/simplejson/__init__.py +++ /dev/null @@ -1,207 +0,0 @@ -r""" -A simple, fast, extensible JSON encoder and decoder - -JSON (JavaScript Object Notation) is a subset of -JavaScript syntax (ECMA-262 3rd edition) used as a lightweight data -interchange format. - -simplejson exposes an API familiar to uses of the standard library -marshal and pickle modules. - -Encoding basic Python object hierarchies:: - - >>> import simplejson - >>> simplejson.dumps(['foo', {'bar': ('baz', None, 1.0, 2)}]) - '["foo", {"bar": ["baz", null, 1.0, 2]}]' - >>> print simplejson.dumps("\"foo\bar") - "\"foo\bar" - >>> print simplejson.dumps(u'\u1234') - "\u1234" - >>> print simplejson.dumps('\\') - "\\" - >>> print simplejson.dumps({"c": 0, "b": 0, "a": 0}, sort_keys=True) - {"a": 0, "b": 0, "c": 0} - >>> from StringIO import StringIO - >>> io = StringIO() - >>> simplejson.dump(['streaming API'], io) - >>> io.getvalue() - '["streaming API"]' - -Decoding JSON:: - - >>> import simplejson - >>> simplejson.loads('["foo", {"bar":["baz", null, 1.0, 2]}]') - [u'foo', {u'bar': [u'baz', None, 1.0, 2]}] - >>> simplejson.loads('"\\"foo\\bar"') - u'"foo\x08ar' - >>> from StringIO import StringIO - >>> io = StringIO('["streaming API"]') - >>> simplejson.load(io) - [u'streaming API'] - -Specializing JSON object decoding:: - - >>> import simplejson - >>> def as_complex(dct): - ... if '__complex__' in dct: - ... return complex(dct['real'], dct['imag']) - ... return dct - ... - >>> simplejson.loads('{"__complex__": true, "real": 1, "imag": 2}', - ... object_hook=as_complex) - (1+2j) - -Extending JSONEncoder:: - - >>> import simplejson - >>> class ComplexEncoder(simplejson.JSONEncoder): - ... def default(self, obj): - ... if isinstance(obj, complex): - ... return [obj.real, obj.imag] - ... return simplejson.JSONEncoder.default(self, obj) - ... - >>> dumps(2 + 1j, cls=ComplexEncoder) - '[2.0, 1.0]' - >>> ComplexEncoder().encode(2 + 1j) - '[2.0, 1.0]' - >>> list(ComplexEncoder().iterencode(2 + 1j)) - ['[', '2.0', ', ', '1.0', ']'] - - -Note that the JSON produced by this module is a subset of YAML, -so it may be used as a serializer for that as well. -""" -__version__ = '1.3' -__all__ = [ - 'dump', 'dumps', 'load', 'loads', - 'JSONDecoder', 'JSONEncoder', -] - -from .decoder import JSONDecoder -from .encoder import JSONEncoder - -def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True, - allow_nan=True, cls=None, **kw): - """ - Serialize ``obj`` as a JSON formatted stream to ``fp`` (a - ``.write()``-supporting file-like object). - - If ``skipkeys`` is ``True`` then ``dict`` keys that are not basic types - (``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``) - will be skipped instead of raising a ``TypeError``. - - If ``ensure_ascii`` is ``False``, then the some chunks written to ``fp`` - may be ``unicode`` instances, subject to normal Python ``str`` to - ``unicode`` coercion rules. Unless ``fp.write()`` explicitly - understands ``unicode`` (as in ``codecs.getwriter()``) this is likely - to cause an error. - - If ``check_circular`` is ``False``, then the circular reference check - for container types will be skipped and a circular reference will - result in an ``OverflowError`` (or worse). - - If ``allow_nan`` is ``False``, then it will be a ``ValueError`` to - serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) - in strict compliance of the JSON specification, instead of using the - JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). - - To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the - ``.default()`` method to serialize additional types), specify it with - the ``cls`` kwarg. - """ - if cls is None: - cls = JSONEncoder - iterable = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii, - check_circular=check_circular, allow_nan=allow_nan, - **kw).iterencode(obj) - # could accelerate with writelines in some versions of Python, at - # a debuggability cost - for chunk in iterable: - fp.write(chunk) - -def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, - allow_nan=True, cls=None, **kw): - """ - Serialize ``obj`` to a JSON formatted ``str``. - - If ``skipkeys`` is ``True`` then ``dict`` keys that are not basic types - (``str``, ``unicode``, ``int``, ``long``, ``float``, ``bool``, ``None``) - will be skipped instead of raising a ``TypeError``. - - If ``ensure_ascii`` is ``False``, then the return value will be a - ``unicode`` instance subject to normal Python ``str`` to ``unicode`` - coercion rules instead of being escaped to an ASCII ``str``. - - If ``check_circular`` is ``False``, then the circular reference check - for container types will be skipped and a circular reference will - result in an ``OverflowError`` (or worse). - - If ``allow_nan`` is ``False``, then it will be a ``ValueError`` to - serialize out of range ``float`` values (``nan``, ``inf``, ``-inf``) in - strict compliance of the JSON specification, instead of using the - JavaScript equivalents (``NaN``, ``Infinity``, ``-Infinity``). - - To use a custom ``JSONEncoder`` subclass (e.g. one that overrides the - ``.default()`` method to serialize additional types), specify it with - the ``cls`` kwarg. - """ - if cls is None: - cls = JSONEncoder - return cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii, - check_circular=check_circular, allow_nan=allow_nan, **kw).encode(obj) - -def load(fp, encoding=None, cls=None, **kw): - """ - Deserialize ``fp`` (a ``.read()``-supporting file-like object containing - a JSON document) to a Python object. - - If the contents of ``fp`` is encoded with an ASCII based encoding other - than utf-8 (e.g. latin-1), then an appropriate ``encoding`` name must - be specified. Encodings that are not ASCII based (such as UCS-2) are - not allowed, and should be wrapped with - ``codecs.getreader(fp)(encoding)``, or simply decoded to a ``unicode`` - object and passed to ``loads()`` - - To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` - kwarg. - """ - if cls is None: - cls = JSONDecoder - return cls(encoding=encoding, **kw).decode(fp.read()) - -def loads(s, encoding=None, cls=None, **kw): - """ - Deserialize ``s`` (a ``str`` or ``unicode`` instance containing a JSON - document) to a Python object. - - If ``s`` is a ``str`` instance and is encoded with an ASCII based encoding - other than utf-8 (e.g. latin-1) then an appropriate ``encoding`` name - must be specified. Encodings that are not ASCII based (such as UCS-2) - are not allowed and should be decoded to ``unicode`` first. - - To use a custom ``JSONDecoder`` subclass, specify it with the ``cls`` - kwarg. - """ - if cls is None: - cls = JSONDecoder - return cls(encoding=encoding, **kw).decode(s) - -def read(s): - """ - json-py API compatibility hook. Use loads(s) instead. - """ - import warnings - warnings.warn("simplejson.loads(s) should be used instead of read(s)", - DeprecationWarning) - return loads(s) - -def write(obj): - """ - json-py API compatibility hook. Use dumps(s) instead. - """ - import warnings - warnings.warn("simplejson.dumps(s) should be used instead of write(s)", - DeprecationWarning) - return dumps(obj) - - diff --git a/src/libs/python/bongo/external/simplejson/decoder.py b/src/libs/python/bongo/external/simplejson/decoder.py deleted file mode 100644 index cb78ba3..0000000 --- a/src/libs/python/bongo/external/simplejson/decoder.py +++ /dev/null @@ -1,271 +0,0 @@ -""" -Implementation of JSONDecoder -""" -import re - -from .scanner import Scanner, pattern - -FLAGS = re.VERBOSE | re.MULTILINE | re.DOTALL - -def _floatconstants(): - import struct - import sys - _BYTES = '7FF80000000000007FF0000000000000'.decode('hex') - if sys.byteorder != 'big': - _BYTES = _BYTES[:8][::-1] + _BYTES[8:][::-1] - nan, inf = struct.unpack('dd', _BYTES) - return nan, inf, -inf - -NaN, PosInf, NegInf = _floatconstants() - -def linecol(doc, pos): - lineno = doc.count('\n', 0, pos) + 1 - if lineno == 1: - colno = pos - else: - colno = pos - doc.rindex('\n', 0, pos) - return lineno, colno - -def errmsg(msg, doc, pos, end=None): - lineno, colno = linecol(doc, pos) - if end is None: - return '%s: line %d column %d (char %d)' % (msg, lineno, colno, pos) - endlineno, endcolno = linecol(doc, end) - return '%s: line %d column %d - line %d column %d (char %d - %d)' % ( - msg, lineno, colno, endlineno, endcolno, pos, end) - -_CONSTANTS = { - '-Infinity': NegInf, - 'Infinity': PosInf, - 'NaN': NaN, - 'true': True, - 'false': False, - 'null': None, -} - -def JSONConstant(match, context, c=_CONSTANTS): - return c[match.group(0)], None -pattern('(-?Infinity|NaN|true|false|null)')(JSONConstant) - -def JSONNumber(match, context): - match = JSONNumber.regex.match(match.string, *match.span()) - integer, frac, exp = match.groups() - if frac or exp: - res = float(integer + (frac or '') + (exp or '')) - else: - res = int(integer) - return res, None -pattern(r'(-?(?:0|[1-9]\d*))(\.\d+)?([eE][-+]?\d+)?')(JSONNumber) - -STRINGCHUNK = re.compile(r'(.*?)(["\\])', FLAGS) -BACKSLASH = { - '"': '"', '\\': '\\', '/': '/', - 'b': '\b', 'f': '\f', 'n': '\n', 'r': '\r', 't': '\t', -} - -DEFAULT_ENCODING = "utf-8" - -def scanstring(s, end, encoding=None, _b=BACKSLASH, _m=STRINGCHUNK.match): - if encoding is None: - encoding = DEFAULT_ENCODING - chunks = [] - _append = chunks.append - begin = end - 1 - while 1: - chunk = _m(s, end) - if chunk is None: - raise ValueError( - errmsg("Unterminated string starting at", s, begin)) - end = chunk.end() - content, terminator = chunk.groups() - if content: - if not isinstance(content, str): - content = str(content, encoding) - _append(content) - if terminator == '"': - break - try: - esc = s[end] - except IndexError: - raise ValueError( - errmsg("Unterminated string starting at", s, begin)) - if esc != 'u': - try: - m = _b[esc] - except KeyError: - raise ValueError( - errmsg("Invalid \\escape: %r" % (esc,), s, end)) - end += 1 - else: - esc = s[end + 1:end + 5] - try: - m = chr(int(esc, 16)) - if len(esc) != 4 or not esc.isalnum(): - raise ValueError - except ValueError: - raise ValueError(errmsg("Invalid \\uXXXX escape", s, end)) - end += 5 - _append(m) - return ''.join(chunks), end - -def JSONString(match, context): - encoding = getattr(context, 'encoding', None) - return scanstring(match.string, match.end(), encoding) -pattern(r'"')(JSONString) - -WHITESPACE = re.compile(r'\s*', FLAGS) - -def JSONObject(match, context, _w=WHITESPACE.match): - pairs = {} - s = match.string - end = _w(s, match.end()).end() - nextchar = s[end:end + 1] - # trivial empty object - if nextchar == '}': - return pairs, end + 1 - if nextchar != '"': - raise ValueError(errmsg("Expecting property name", s, end)) - end += 1 - encoding = getattr(context, 'encoding', None) - while True: - key, end = scanstring(s, end, encoding) - end = _w(s, end).end() - if s[end:end + 1] != ':': - raise ValueError(errmsg("Expecting : delimiter", s, end)) - end = _w(s, end + 1).end() - try: - value, end = next(JSONScanner.iterscan(s, idx=end)) - except StopIteration: - raise ValueError(errmsg("Expecting object", s, end)) - pairs[key] = value - end = _w(s, end).end() - nextchar = s[end:end + 1] - end += 1 - if nextchar == '}': - break - if nextchar != ',': - raise ValueError(errmsg("Expecting , delimiter", s, end - 1)) - end = _w(s, end).end() - nextchar = s[end:end + 1] - end += 1 - if nextchar != '"': - raise ValueError(errmsg("Expecting property name", s, end - 1)) - object_hook = getattr(context, 'object_hook', None) - if object_hook is not None: - pairs = object_hook(pairs) - return pairs, end -pattern(r'{')(JSONObject) - -def JSONArray(match, context, _w=WHITESPACE.match): - values = [] - s = match.string - end = _w(s, match.end()).end() - # look-ahead for trivial empty array - nextchar = s[end:end + 1] - if nextchar == ']': - return values, end + 1 - while True: - try: - value, end = next(JSONScanner.iterscan(s, idx=end)) - except StopIteration: - raise ValueError(errmsg("Expecting object", s, end)) - values.append(value) - end = _w(s, end).end() - nextchar = s[end:end + 1] - end += 1 - if nextchar == ']': - break - if nextchar != ',': - raise ValueError(errmsg("Expecting , delimiter", s, end)) - end = _w(s, end).end() - return values, end -pattern(r'\[')(JSONArray) - -ANYTHING = [ - JSONObject, - JSONArray, - JSONString, - JSONConstant, - JSONNumber, -] - -JSONScanner = Scanner(ANYTHING) - -class JSONDecoder(object): - """ - Simple JSON decoder - - Performs the following translations in decoding: - - +---------------+-------------------+ - | JSON | Python | - +===============+===================+ - | object | dict | - +---------------+-------------------+ - | array | list | - +---------------+-------------------+ - | string | unicode | - +---------------+-------------------+ - | number (int) | int, long | - +---------------+-------------------+ - | number (real) | float | - +---------------+-------------------+ - | true | True | - +---------------+-------------------+ - | false | False | - +---------------+-------------------+ - | null | None | - +---------------+-------------------+ - - It also understands ``NaN``, ``Infinity``, and ``-Infinity`` as - their corresponding ``float`` values, which is outside the JSON spec. - """ - - _scanner = Scanner(ANYTHING) - __all__ = ['__init__', 'decode', 'raw_decode'] - - def __init__(self, encoding=None, object_hook=None): - """ - ``encoding`` determines the encoding used to interpret any ``str`` - objects decoded by this instance (utf-8 by default). It has no - effect when decoding ``unicode`` objects. - - Note that currently only encodings that are a superset of ASCII work, - strings of other encodings should be passed in as ``unicode``. - - ``object_hook``, if specified, will be called with the result - of every JSON object decoded and its return value will be used in - place of the given ``dict``. This can be used to provide custom - deserializations (e.g. to support JSON-RPC class hinting). - """ - self.encoding = encoding - self.object_hook = object_hook - - def decode(self, s, _w=WHITESPACE.match): - """ - Return the Python representation of ``s`` (a ``str`` or ``unicode`` - instance containing a JSON document) - """ - obj, end = self.raw_decode(s, idx=_w(s, 0).end()) - end = _w(s, end).end() - if end != len(s): - raise ValueError(errmsg("Extra data", s, end, len(s))) - return obj - - def raw_decode(self, s, **kw): - """ - Decode a JSON document from ``s`` (a ``str`` or ``unicode`` beginning - with a JSON document) and return a 2-tuple of the Python - representation and the index in ``s`` where the document ended. - - This can be used to decode a JSON document from a string that may - have extraneous data at the end. - """ - kw.setdefault('context', self) - try: - obj, end = next(self._scanner.iterscan(s, **kw)) - except StopIteration: - raise ValueError("No JSON object could be decoded") - return obj, end - -__all__ = ['JSONDecoder'] diff --git a/src/libs/python/bongo/external/simplejson/encoder.py b/src/libs/python/bongo/external/simplejson/encoder.py deleted file mode 100644 index 4868cdb..0000000 --- a/src/libs/python/bongo/external/simplejson/encoder.py +++ /dev/null @@ -1,289 +0,0 @@ -""" -Implementation of JSONEncoder -""" -import re - -# this should match any kind of infinity -INFCHARS = re.compile(r'[infINF]') -ESCAPE = re.compile(r'[\x00-\x19\\"\b\f\n\r\t]') -ESCAPE_ASCII = re.compile(r'([\\"]|[^\ -~])') -ESCAPE_DCT = { - '\\': '\\\\', - '"': '\\"', - '\b': '\\b', - '\f': '\\f', - '\n': '\\n', - '\r': '\\r', - '\t': '\\t', -} -for i in range(20): - ESCAPE_DCT.setdefault(chr(i), '\\u%04x' % (i,)) - -def floatstr(o, allow_nan=True): - s = str(o) - # If the first non-sign is a digit then it's not a special value - if (o < 0.0 and s[1].isdigit()) or s[0].isdigit(): - return s - elif not allow_nan: - raise ValueError("Out of range float values are not JSON compliant: %r" - % (o,)) - # These are the string representations on the platforms I've tried - if s == 'nan': - return 'NaN' - if s == 'inf': - return 'Infinity' - if s == '-inf': - return '-Infinity' - # NaN should either be inequal to itself, or equal to everything - if o != o or o == 0.0: - return 'NaN' - # Last ditch effort, assume inf - if o < 0: - return '-Infinity' - return 'Infinity' - -def encode_basestring(s): - """ - Return a JSON representation of a Python string - """ - def replace(match): - return ESCAPE_DCT[match.group(0)] - return '"' + ESCAPE.sub(replace, s) + '"' - -def encode_basestring_ascii(s): - def replace(match): - s = match.group(0) - try: - return ESCAPE_DCT[s] - except KeyError: - return '\\u%04x' % (ord(s),) - return '"' + str(ESCAPE_ASCII.sub(replace, s)) + '"' - - -class JSONEncoder(object): - """ - Extensible JSON encoder for Python data structures. - - Supports the following objects and types by default: - - +-------------------+---------------+ - | Python | JSON | - +===================+===============+ - | dict | object | - +-------------------+---------------+ - | list, tuple | array | - +-------------------+---------------+ - | str, unicode | string | - +-------------------+---------------+ - | int, long, float | number | - +-------------------+---------------+ - | True | true | - +-------------------+---------------+ - | False | false | - +-------------------+---------------+ - | None | null | - +-------------------+---------------+ - - To extend this to recognize other objects, subclass and implement a - ``.default()`` method with another method that returns a serializable - object for ``o`` if possible, otherwise it should call the superclass - implementation (to raise ``TypeError``). - """ - __all__ = ['__init__', 'default', 'encode', 'iterencode'] - def __init__(self, skipkeys=False, ensure_ascii=True, - check_circular=True, allow_nan=True, sort_keys=False): - """ - Constructor for JSONEncoder, with sensible defaults. - - If skipkeys is False, then it is a TypeError to attempt - encoding of keys that are not str, int, long, float or None. If - skipkeys is True, such items are simply skipped. - - If ensure_ascii is True, the output is guaranteed to be str - objects with all incoming unicode characters escaped. If - ensure_ascii is false, the output will be unicode object. - - If check_circular is True, then lists, dicts, and custom encoded - objects will be checked for circular references during encoding to - prevent an infinite recursion (which would cause an OverflowError). - Otherwise, no such check takes place. - - If allow_nan is True, then NaN, Infinity, and -Infinity will be - encoded as such. This behavior is not JSON specification compliant, - but is consistent with most JavaScript based encoders and decoders. - Otherwise, it will be a ValueError to encode such floats. - - If sort_keys is True, then the output of dictionaries will be - sorted by key; this is useful for regression tests to ensure - that JSON serializations can be compared on a day-to-day basis. - """ - - self.skipkeys = skipkeys - self.ensure_ascii = ensure_ascii - self.check_circular = check_circular - self.allow_nan = allow_nan - self.sort_keys = sort_keys - - def _iterencode_list(self, lst, markers=None): - if not lst: - yield '[]' - return - if markers is not None: - markerid = id(lst) - if markerid in markers: - raise ValueError("Circular reference detected") - markers[markerid] = lst - yield '[' - first = True - for value in lst: - if first: - first = False - else: - yield ', ' - for chunk in self._iterencode(value, markers): - yield chunk - yield ']' - if markers is not None: - del markers[markerid] - - def _iterencode_dict(self, dct, markers=None): - if not dct: - yield '{}' - return - if markers is not None: - markerid = id(dct) - if markerid in markers: - raise ValueError("Circular reference detected") - markers[markerid] = dct - yield '{' - first = True - if self.ensure_ascii: - encoder = encode_basestring_ascii - else: - encoder = encode_basestring - allow_nan = self.allow_nan - if self.sort_keys: - keys = list(dct.keys()) - keys.sort() - items = [(k,dct[k]) for k in keys] - else: - items = iter(dct.items()) - for key, value in items: - if isinstance(key, str): - pass - # JavaScript is weakly typed for these, so it makes sense to - # also allow them. Many encoders seem to do something like this. - elif isinstance(key, float): - key = floatstr(key, allow_nan) - elif isinstance(key, int): - key = str(key) - elif key is True: - key = 'true' - elif key is False: - key = 'false' - elif key is None: - key = 'null' - elif self.skipkeys: - continue - else: - raise TypeError("key %r is not a string" % (key,)) - if first: - first = False - else: - yield ', ' - yield encoder(key) - yield ': ' - for chunk in self._iterencode(value, markers): - yield chunk - yield '}' - if markers is not None: - del markers[markerid] - - def _iterencode(self, o, markers=None): - if isinstance(o, str): - if self.ensure_ascii: - encoder = encode_basestring_ascii - else: - encoder = encode_basestring - yield encoder(o) - elif o is None: - yield 'null' - elif o is True: - yield 'true' - elif o is False: - yield 'false' - elif isinstance(o, int): - yield str(o) - elif isinstance(o, float): - yield floatstr(o, self.allow_nan) - elif isinstance(o, (list, tuple)): - for chunk in self._iterencode_list(o, markers): - yield chunk - elif isinstance(o, dict): - for chunk in self._iterencode_dict(o, markers): - yield chunk - else: - if markers is not None: - markerid = id(o) - if markerid in markers: - raise ValueError("Circular reference detected") - markers[markerid] = o - for chunk in self._iterencode_default(o, markers): - yield chunk - if markers is not None: - del markers[markerid] - - def _iterencode_default(self, o, markers=None): - newobj = self.default(o) - return self._iterencode(newobj, markers) - - def default(self, o): - """ - Implement this method in a subclass such that it returns - a serializable object for ``o``, or calls the base implementation - (to raise a ``TypeError``). - - For example, to support arbitrary iterators, you could - implement default like this:: - - def default(self, o): - try: - iterable = iter(o) - except TypeError: - pass - else: - return list(iterable) - return JSONEncoder.default(self, o) - """ - raise TypeError("%r is not JSON serializable" % (o,)) - - def encode(self, o): - """ - Return a JSON string representation of a Python data structure. - - >>> JSONEncoder().encode({"foo": ["bar", "baz"]}) - '{"foo":["bar", "baz"]}' - """ - # This doesn't pass the iterator directly to ''.join() because it - # sucks at reporting exceptions. It's going to do this internally - # anyway because it uses PySequence_Fast or similar. - chunks = list(self.iterencode(o)) - return ''.join(chunks) - - def iterencode(self, o): - """ - Encode the given object and yield each string - representation as available. - - For example:: - - for chunk in JSONEncoder().iterencode(bigobject): - mysocket.write(chunk) - """ - if self.check_circular: - markers = {} - else: - markers = None - return self._iterencode(o, markers) - -__all__ = ['JSONEncoder'] diff --git a/src/libs/python/bongo/external/simplejson/jsonfilter.py b/src/libs/python/bongo/external/simplejson/jsonfilter.py deleted file mode 100644 index 990bc60..0000000 --- a/src/libs/python/bongo/external/simplejson/jsonfilter.py +++ /dev/null @@ -1,40 +0,0 @@ -import simplejson -import cgi - -class JSONFilter(object): - def __init__(self, app, mime_type='text/x-json'): - self.app = app - self.mime_type = mime_type - - def __call__(self, environ, start_response): - # Read JSON POST input to jsonfilter.json if matching mime type - response = {'status': '200 OK', 'headers': []} - def json_start_response(status, headers): - response['status'] = status - response['headers'].extend(headers) - environ['jsonfilter.mime_type'] = self.mime_type - if environ.get('REQUEST_METHOD', '') == 'POST': - if environ.get('CONTENT_TYPE', '') == self.mime_type: - args = [_ for _ in [environ.get('CONTENT_LENGTH')] if _] - data = environ['wsgi.input'].read(*list(map(int, args))) - environ['jsonfilter.json'] = simplejson.loads(data) - res = simplejson.dumps(self.app(environ, json_start_response)) - jsonp = cgi.parse_qs(environ.get('QUERY_STRING', '')).get('jsonp') - if jsonp: - content_type = 'text/javascript' - res = ''.join(jsonp + ['(', res, ')']) - elif 'Opera' in environ.get('HTTP_USER_AGENT', ''): - # Opera has bunk XMLHttpRequest support for most mime types - content_type = 'text/plain' - else: - content_type = self.mime_type - headers = [ - ('Content-type', content_type), - ('Content-length', len(res)), - ] - headers.extend(response['headers']) - start_response(response['status'], headers) - return [res] - -def factory(app, global_conf, **kw): - return JSONFilter(app, **kw) diff --git a/src/libs/python/bongo/external/simplejson/scanner.py b/src/libs/python/bongo/external/simplejson/scanner.py deleted file mode 100644 index 64f4999..0000000 --- a/src/libs/python/bongo/external/simplejson/scanner.py +++ /dev/null @@ -1,63 +0,0 @@ -""" -Iterator based sre token scanner -""" -import sre_parse, sre_compile, sre_constants -from sre_constants import BRANCH, SUBPATTERN -from re import VERBOSE, MULTILINE, DOTALL -import re - -__all__ = ['Scanner', 'pattern'] - -FLAGS = (VERBOSE | MULTILINE | DOTALL) -class Scanner(object): - def __init__(self, lexicon, flags=FLAGS): - self.actions = [None] - # combine phrases into a compound pattern - s = sre_parse.Pattern() - s.flags = flags - p = [] - for idx, token in enumerate(lexicon): - phrase = token.pattern - try: - subpattern = sre_parse.SubPattern(s, - [(SUBPATTERN, (idx + 1, sre_parse.parse(phrase, flags)))]) - except sre_constants.error: - raise - p.append(subpattern) - self.actions.append(token) - - p = sre_parse.SubPattern(s, [(BRANCH, (None, p))]) - self.scanner = sre_compile.compile(p) - - - def iterscan(self, string, idx=0, context=None): - """ - Yield match, end_idx for each match - """ - match = self.scanner.scanner(string, idx).match - actions = self.actions - lastend = idx - end = len(string) - while True: - m = match() - if m is None: - break - matchbegin, matchend = m.span() - if lastend == matchend: - break - action = actions[m.lastindex] - if action is not None: - rval, next_pos = action(m, context) - if next_pos is not None and next_pos != matchend: - # "fast forward" the scanner - matchend = next_pos - match = self.scanner.scanner(string, matchend).match - yield rval, matchend - lastend = matchend - -def pattern(pattern, flags=FLAGS): - def decorator(fn): - fn.pattern = pattern - fn.regex = re.compile(pattern, flags) - return fn - return decorator diff --git a/src/libs/python/bongo/external/simpletal/simpleTAL.py b/src/libs/python/bongo/external/simpletal/simpleTAL.py index 84b00ac..536adfc 100644 --- a/src/libs/python/bongo/external/simpletal/simpleTAL.py +++ b/src/libs/python/bongo/external/simpletal/simpleTAL.py @@ -107,7 +107,7 @@ METAL_FILL_SLOT=16 METAL_DEFINE_MACRO=17 METAL_NAME_REGEX = re.compile ("[a-zA-Z_][a-zA-Z0-9_]*") -SINGLETON_XML_REGEX = re.compile ('^<[^\s/>]+(?:\s*[^=>]+="[^">]+")*\s*/>') +SINGLETON_XML_REGEX = re.compile (r'^<[^\s/>]+(?:\s*[^=>]+="[^">]+")*\s*/>') ENTITY_REF_REGEX = re.compile (r'(?:&[a-zA-Z][\-\.a-zA-Z0-9]*[^\-\.a-zA-Z0-9])|(?:&#[xX]?[a-eA-E0-9]*[^0-9a-eA-E])') # The list of elements in HTML that can not have end tags - done as a dictionary for fast diff --git a/src/libs/python/bongo/external/vobject/__init__.py b/src/libs/python/bongo/external/vobject/__init__.py deleted file mode 100644 index 2e7f088..0000000 --- a/src/libs/python/bongo/external/vobject/__init__.py +++ /dev/null @@ -1,86 +0,0 @@ -""" -VObject Overview -================ - vobject parses vCard or vCalendar files, returning a tree of Python objects. - It also provids an API to create vCard or vCalendar data structures which - can then be serialized. - - Parsing existing streams - ------------------------ - Streams containing one or many L{Component}s can be - parsed using L{readComponents}. As each Component - is parsed, vobject will attempt to give it a L{Behavior}. - If an appropriate Behavior is found, any base64, quoted-printable, or - backslash escaped data will automatically be decoded. Dates and datetimes - will be transformed to datetime.date or datetime.datetime instances. - Components containing recurrence information will have a special rruleset - attribute (a dateutil.rrule.rruleset instance). - - Validation - ---------- - L{Behavior} classes implement validation for - L{Component}s. To validate, an object must have all - required children. There (TODO: will be) a toggle to raise an exception or - just log unrecognized, non-experimental children and parameters. - - Creating objects programatically - -------------------------------- - A L{Component} can be created from scratch. No encoding - is necessary, serialization will encode data automatically. Factory - functions (TODO: will be) available to create standard objects. - - Serializing objects - ------------------- - Serialization: - - Looks for missing required children that can be automatically generated, - like a UID or a PRODID, and adds them - - Encodes all values that can be automatically encoded - - Checks to make sure the object is valid (unless this behavior is - explicitly disabled) - - Appends the serialized object to a buffer, or fills a new - buffer and returns it - - Examples - -------- - - >>> import datetime - >>> import dateutil.rrule as rrule - >>> x = iCalendar() - >>> x.add('vevent') - - >>> x - ]> - >>> v = x.vevent - >>> utc = icalendar.utc - >>> v.add('dtstart').value = datetime.datetime(2004, 12, 15, 14, tzinfo = utc) - >>> v - ]> - >>> x - ]>]> - >>> newrule = rrule.rruleset() - >>> newrule.rrule(rrule.rrule(rrule.WEEKLY, count=2, dtstart=v.dtstart.value)) - >>> v.rruleset = newrule - >>> list(v.rruleset) - [datetime.datetime(2004, 12, 15, 14, 0, tzinfo=tzutc()), datetime.datetime(2004, 12, 22, 14, 0, tzinfo=tzutc())] - >>> v.add('uid').value = "randomuid@MYHOSTNAME" - >>> print x.serialize() - BEGIN:VCALENDAR - VERSION:2.0 - PRODID:-//PYVOBJECT//NONSGML Version 1//EN - BEGIN:VEVENT - UID:randomuid@MYHOSTNAME - DTSTART:20041215T140000Z - RRULE:FREQ=WEEKLY;COUNT=2 - END:VEVENT - END:VCALENDAR - -""" - -from . import base, icalendar, vcard -from .base import readComponents, readOne, newFromBehavior - -def iCalendar(): - return newFromBehavior('vcalendar', '2.0') - -def vCard(): - return newFromBehavior('vcard', '3.0') diff --git a/src/libs/python/bongo/external/vobject/base.py b/src/libs/python/bongo/external/vobject/base.py deleted file mode 100644 index bcee5ef..0000000 --- a/src/libs/python/bongo/external/vobject/base.py +++ /dev/null @@ -1,950 +0,0 @@ -"""vobject module for reading vCard and vCalendar files.""" - -# generator support for Python 2.3 - - -import re -import sys -import logging -import io -import string -import exceptions -import codecs - -#------------------------------------ Logging ---------------------------------- -logger = logging.getLogger('vobject') -if not logger.handlers: - handler=logging.StreamHandler() - formatter = logging.Formatter('%(name)s %(levelname)s %(message)s') - handler.setFormatter(formatter) - logger.addHandler(handler) -logger.setLevel(logging.ERROR) # Log errors -DEBUG = False # Don't waste time on debug calls -#----------------------------------- Constants --------------------------------- -CR = chr(13) -LF = chr(10) -CRLF = CR + LF -SPACE = chr(32) -TAB = chr(9) -SPACEORTAB = SPACE + TAB -#-------------------------------- Useful modules ------------------------------- -# use doctest, it kills two birds with one stone and docstrings often become -# more readable to boot (see parseLine's docstring). -# use logging, then when debugging we can just set our verbosity. -# use epydoc syntax for documenting code, please document every class and non- -# trivial method (see http://epydoc.sourceforge.net/epytext.html -# and http://epydoc.sourceforge.net/fields.html). Also, please -# follow http://www.python.org/peps/pep-0257.html for docstrings. -#------------------------------------------------------------------------------- - -#--------------------------------- Main classes -------------------------------- -class VBase(object): - """Base class for ContentLine and Component. - - @ivar behavior: - The Behavior class associated with this object, which controls - validation, transformations, and encoding. - @ivar parentBehavior: - The object's parent's behavior, or None if no behaviored parent exists. - @ivar isNative: - Boolean describing whether this component is a Native instance. - @ivar group: - An optional group prefix, should be used only to indicate sort order in - vCards, according to RFC2426 - """ - def __init__(self, group=None, *args, **kwds): - super(VBase, self).__init__(*args, **kwds) - self.group = group - self.behavior = None - self.parentBehavior = None - self.isNative = False - - def validate(self, *args, **kwds): - """Call the behavior's validate method, or return True.""" - if self.behavior: - return self.behavior.validate(self, *args, **kwds) - else: return True - - def getChildren(self): - """Return an iterable containing the contents of the object.""" - return [] - - def clearBehavior(self, cascade=True): - """Set behavior to None. Do for all descendants if cascading.""" - self.behavior=None - if cascade: self.transformChildrenFromNative() - - def autoBehavior(self, cascade=False): - """Set behavior if name is in self.parentBehavior.knownChildren. - - If cascade is True, unset behavior and parentBehavior for all - descendants, then recalculate behavior and parentBehavior. - - """ - parentBehavior = self.parentBehavior - if parentBehavior is not None: - knownChildTup = parentBehavior.knownChildren.get(self.name, None) - if knownChildTup is not None: - behavior = getBehavior(self.name, knownChildTup[2]) - if behavior is not None: - self.setBehavior(behavior, cascade) - if isinstance(self, ContentLine) and self.encoded: - self.behavior.decode(self) - - def setBehavior(self, behavior, cascade=True): - """Set behavior. If cascade is True, autoBehavior all descendants.""" - self.behavior=behavior - if cascade: - for obj in self.getChildren(): - obj.parentBehavior=behavior - obj.autoBehavior(True) - - def transformToNative(self): - """Transform this object into a custom VBase subclass. - - transformToNative should always return a representation of this object. - It may do so by modifying self in place then returning self, or by - creating a new object. - - """ - if self.isNative or not self.behavior or not self.behavior.hasNative: - return self - else: - try: - return self.behavior.transformToNative(self) - except Exception as e: - # wrap errors in transformation in a ParseError - lineNumber = getattr(self, 'lineNumber', None) - if isinstance(e, ParseError): - if lineNumber is not None: - e.lineNumber = lineNumber - raise - else: - msg = "In transformToNative, unhandled exception: %s: %s" - msg = msg % (sys.exc_info()[0], sys.exc_info()[1]) - new_error = ParseError(msg, lineNumber) - raise ParseError(new_error).with_traceback(sys.exc_info()[2]) - - - def transformFromNative(self): - """Return self transformed into a ContentLine or Component if needed. - - May have side effects. If it does, transformFromNative and - transformToNative MUST have perfectly inverse side effects. Allowing - such side effects is convenient for objects whose transformations only - change a few attributes. - - Note that it isn't always possible for transformFromNative to be a - perfect inverse of transformToNative, in such cases transformFromNative - should return a new object, not self after modifications. - - """ - if self.isNative and self.behavior and self.behavior.hasNative: - try: - return self.behavior.transformFromNative(self) - except Exception as e: - # wrap errors in transformation in a NativeError - lineNumber = getattr(self, 'lineNumber', None) - if isinstance(e, NativeError): - if lineNumber is not None: - e.lineNumber = lineNumber - raise - else: - msg = "In transformFromNative, unhandled exception: %s: %s" - msg = msg % (sys.exc_info()[0], sys.exc_info()[1]) - new_error = NativeError(msg, lineNumber) - raise NativeError(new_error).with_traceback(sys.exc_info()[2]) - else: return self - - def transformChildrenToNative(self): - """Recursively replace children with their native representation.""" - pass - - def transformChildrenFromNative(self, clearBehavior=True): - """Recursively transform native children to vanilla representations.""" - pass - - def serialize(self, buf=None, lineLength=75, validate=True): - """Serialize to buf if it exists, otherwise return a string. - - Use self.behavior.serialize if behavior exists. - - """ - undoTransform = False - if self.behavior: - if DEBUG: logger.debug("serializing %s with behavior" % self.name) - self.behavior.generateImplicitParameters(self) - if validate: self.behavior.validate(self, raiseException=True) - if self.isNative: - transformed = self.transformFromNative() - undoTransform = True - else: - transformed = self - out = self.behavior.serialize(transformed, buf, lineLength) - if undoTransform: self.transformToNative() - return out - else: - if DEBUG: logger.debug("serializing %s without behavior" % self.name) - return defaultSerialize(self, buf, lineLength) - -def ascii(s): - return str(s).encode('ascii', 'replace') - -class ContentLine(VBase): - """Holds one content line for formats like vCard and vCalendar. - - For example:: - - - @ivar name: - The uppercased name of the contentline. - @ivar params: - A dictionary of parameters and associated lists of values (the list may - be empty for empty parameters). - @ivar value: - The value of the contentline. - @ivar singletonparams: - A list of parameters for which it's unclear if the string represents the - parameter name or the parameter value. In vCard 2.1, "The value string - can be specified alone in those cases where the value is unambiguous". - This is crazy, but we have to deal with it. - @ivar encoded: - A boolean describing whether the data in the content line is encoded. - Generally, text read from a serialized vCard or vCalendar should be - considered encoded. Data added programmatically should not be encoded. - @ivar lineNumber: - An optional line number associated with the contentline. - """ - def __init__(self, name, params, value, group=None, - encoded=False, isNative=False, - lineNumber = None, *args, **kwds): - """Take output from parseLine, convert params list to dictionary.""" - # group is used as a positional argument to match parseLine's return - super(ContentLine, self).__init__(group, *args, **kwds) - self.name = name.upper() - self.value = value - self.encoded = encoded - self.params = {} - self.singletonparams = [] - self.isNative = isNative - self.lineNumber = lineNumber - def updateTable(x): - if len(x) == 1: - self.singletonparams += x - else: - paramlist = self.params.setdefault(x[0].upper(), []) - paramlist.extend(x[1:]) - list(map(updateTable, params)) - qp = False - if 'ENCODING' in self.params: - if 'QUOTED-PRINTABLE' in self.params['ENCODING']: - qp = True - self.params['ENCODING'].remove('QUOTED-PRINTABLE') - if 0==len(self.params['ENCODING']): - del self.params['ENCODING'] - if 'QUOTED-PRINTABLE' in self.singletonparams: - qp = True - self.singletonparams.remove('QUOTED-PRINTABLE') - if qp: - self.value = str(self.value).decode('quoted-printable') - - def __eq__(self, other): - try: - return (self.name == other.name) and (self.params == other.params) and (self.value == other.value) - except: - return False - - def __getattr__(self, name): - """Make params accessible via self.foo_param or self.foo_paramlist. - - Underscores, legal in python variable names, are converted to dashes, - which are legal in IANA tokens. - - """ - try: - if name.endswith('_param'): - return self.params[name[:-6].upper().replace('_', '-')][0] - elif name.endswith('_paramlist'): - return self.params[name[:-10].upper().replace('_', '-')] - else: - raise exceptions.AttributeError(name) - except KeyError: - raise exceptions.AttributeError(name) - - def __setattr__(self, name, value): - """Make params accessible via self.foo_param or self.foo_paramlist. - - Underscores, legal in python variable names, are converted to dashes, - which are legal in IANA tokens. - - """ - if name.endswith('_param'): - if type(value) == list: - self.params[name[:-6].upper().replace('_', '-')] = value - else: - self.params[name[:-6].upper().replace('_', '-')] = [value] - elif name.endswith('_paramlist'): - if type(value) == list: - self.params[name[:-10].upper().replace('_', '-')] = value - else: - raise VObjectError("Parameter list set to a non-list") - else: - prop = getattr(self.__class__, name, None) - if isinstance(prop, property): - prop.fset(self, value) - else: - object.__setattr__(self, name, value) - - def __delattr__(self, name): - try: - if name.endswith('_param'): - del self.params[name[:-6].upper().replace('_', '-')] - elif name.endswith('_paramlist'): - del self.params[name[:-10].upper().replace('_', '-')] - else: - object.__delattr__(self, name) - except KeyError: - raise exceptions.AttributeError(name) - - def __str__(self): - return "<"+ascii(self.name)+ascii(self.params)+ascii(self.value)+">" - - def __repr__(self): - return self.__str__().replace('\n', '\\n') - - def prettyPrint(self, level = 0, tabwidth=3): - pre = ' ' * level * tabwidth - print(pre, self.name + ":", ascii(self.value)) - if self.params: - lineKeys= list(self.params.keys()) - print(pre, "params for ", self.name +':') - for aKey in lineKeys: - print(pre + ' ' * tabwidth, aKey, ascii(self.params[aKey])) - -class Component(VBase): - """A complex property that can contain multiple ContentLines. - - For our purposes, a component must start with a BEGIN:xxxx line and end with - END:xxxx, or have a PROFILE:xxx line if a top-level component. - - @ivar contents: - A dictionary of lists of Component or ContentLine instances. The keys - are the lowercased names of child ContentLines or Components. - Note that BEGIN and END ContentLines are not included in contents. - @ivar name: - Uppercase string used to represent this Component, i.e VCARD if the - serialized object starts with BEGIN:VCARD. - @ivar useBegin: - A boolean flag determining whether BEGIN: and END: lines should - be serialized. - - """ - def __init__(self, name=None, *args, **kwds): - super(Component, self).__init__(*args, **kwds) - self.contents = {} - if name: - self.name=name.upper() - self.useBegin = True - else: - self.name = '' - self.useBegin = False - - self.autoBehavior() - - def setProfile(self, name): - """Assign a PROFILE to this unnamed component. - - Used by vCard, not by vCalendar. - - """ - if self.name or self.useBegin: - if self.name == name: return - raise VObjectError("This component already has a PROFILE or uses BEGIN.") - self.name = name.upper() - - def __getattr__(self, name): - """For convenience, make self.contents directly accessible. - - Underscores, legal in python variable names, are converted to dashes, - which are legal in IANA tokens. - - """ - try: - if name.endswith('_list'): - return self.contents[name[:-5].replace('_', '-')] - else: - return self.contents[name.replace('_', '-')][0] - except KeyError: - raise exceptions.AttributeError(name) - - normal_attributes = ['contents','name','behavior','parentBehavior','group'] - def __setattr__(self, name, value): - """For convenience, make self.contents directly accessible. - - Underscores, legal in python variable names, are converted to dashes, - which are legal in IANA tokens. - - """ - if name not in self.normal_attributes and name.lower()==name: - if type(value) == list: - self.contents[name.replace('_', '-')] = value - elif name.endswith('_list'): - raise VObjectError("Component list set to a non-list") - else: - self.contents[name.replace('_', '-')] = [value] - else: - prop = getattr(self.__class__, name, None) - if isinstance(prop, property): - prop.fset(self, value) - else: - object.__setattr__(self, name, value) - - def __delattr__(self, name): - try: - if name not in self.normal_attributes and name.lower()==name: - if name.endswith('_list'): - del self.contents[name[:-5].replace('_', '-')] - else: - del self.contents[name.replace('_', '-')] - else: - object.__delattr__(self, name) - except KeyError: - raise exceptions.AttributeError(name) - - def add(self, objOrName, group = None): - """Add objOrName to contents, set behavior if it can be inferred. - - If objOrName is a string, create an empty component or line based on - behavior. If no behavior is found for the object, add a ContentLine. - - group is an optional prefix to the name of the object (see - RFC 2425). - """ - if isinstance(objOrName, VBase): - obj = objOrName - if self.behavior: - obj.parentBehavior = self.behavior - obj.autoBehavior(True) - else: - name = objOrName.upper() - try: - id=self.behavior.knownChildren[name][2] - behavior = getBehavior(name, id) - if behavior.isComponent: - obj = Component(name) - else: - obj = ContentLine(name, [], '', group) - obj.parentBehavior = self.behavior - obj.behavior = behavior - obj = obj.transformToNative() - except (KeyError, AttributeError): - obj = ContentLine(objOrName, [], '', group) - self.contents.setdefault(obj.name.lower(), []).append(obj) - return obj - - def getChildren(self): - """Return an iterable of all children.""" - for objList in list(self.contents.values()): - for obj in objList: yield obj - - def components(self): - """Return an iterable of all Component children.""" - return [i for i in self.getChildren() if isinstance(i, Component)] - - def lines(self): - """Return an iterable of all ContentLine children.""" - return [i for i in self.getChildren() if isinstance(i, ContentLine)] - - def sortChildKeys(self): - try: - first = [s for s in self.behavior.sortFirst if s in self.contents] - except: - first = [] - - items = [k for k in list(self.contents.keys()) if k not in first] - items.sort() - return first + items - - def getSortedChildren(self): - return [obj for k in self.sortChildKeys() for obj in self.contents[k]] - - def setBehaviorFromVersionLine(self, versionLine): - """Set behavior if one matches name, versionLine.value.""" - v=getBehavior(self.name, versionLine.value) - if v: self.setBehavior(v) - - def transformChildrenToNative(self): - """Recursively replace children with their native representation.""" - #sort to get dependency order right, like vtimezone before vevent - for childArray in [self.contents[k] for k in self.sortChildKeys()]: - for i in range(len(childArray)): - childArray[i]=childArray[i].transformToNative() - childArray[i].transformChildrenToNative() - - def transformChildrenFromNative(self, clearBehavior=True): - """Recursively transform native children to vanilla representations.""" - for childArray in list(self.contents.values()): - for i in range(len(childArray)): - childArray[i]=childArray[i].transformFromNative() - childArray[i].transformChildrenFromNative(clearBehavior) - if clearBehavior: - childArray[i].behavior = None - childArray[i].parentBehavior = None - - def __str__(self): - if self.name: - return "<" + self.name + "| " + str(self.getSortedChildren()) + ">" - else: - return '<' + '*unnamed*' + '| ' + str(self.getSortedChildren()) + '>' - - def __repr__(self): - return self.__str__() - - def prettyPrint(self, level = 0, tabwidth=3): - pre = ' ' * level * tabwidth - print(pre, self.name) - if isinstance(self, Component): - for line in self.getChildren(): - line.prettyPrint(level + 1, tabwidth) - print() - -class VObjectError(Exception): - def __init__(self, message, lineNumber=None): - self.message = message - if lineNumber is not None: - self.lineNumber = lineNumber - def __str__(self): - if hasattr(self, 'lineNumber'): - return "At line %s: %s" % \ - (self.lineNumber, self.message) - else: - return repr(self.message) - -class ParseError(VObjectError): - pass - -class ValidateError(VObjectError): - pass - -class NativeError(VObjectError): - pass - -#-------------------------- Parsing functions ---------------------------------- - -# parseLine regular expressions - -patterns = {} - -patterns['name'] = '[a-zA-Z0-9\-]+' -patterns['safe_char'] = '[^";:,]' -patterns['qsafe_char'] = '[^"]' - -# the combined Python string replacement and regex syntax is a little confusing; -# remember that %(foobar)s is replaced with patterns['foobar'], so for instance -# param_value is any number of safe_chars or any number of qsaf_chars surrounded -# by double quotes. - -patterns['param_value'] = ' "%(qsafe_char)s * " | %(safe_char)s * ' % patterns - - -# get a tuple of two elements, one will be empty, the other will have the value -patterns['param_value_grouped'] = """ -" ( %(qsafe_char)s * )" | ( %(safe_char)s + ) -""" % patterns - -# get a parameter and its values, without any saved groups -patterns['param'] = r""" -; (?: %(name)s ) # parameter name -(?: = (?: %(param_value)s|, ))? -""" % patterns - -# get a parameter, saving groups for name and value (value still needs parsing) -patterns['params_grouped'] = r""" -; ( %(name)s ) - -(?: = - ( - (?: (?: %(param_value)s ) )? # 0 or more parameter values, multiple - (?: , (?: %(param_value)s ) )* # parameters are comma separated - ) -)? -""" % patterns - -# get a full content line, break it up into group, name, parameters, and value -patterns['line'] = r""" -^ ((?P %(name)s)\.)?(?P %(name)s) # name group - (?P (?: %(param)s )* ) # params group (may be empty) -: (?P .* )$ # value group -""" % patterns - -' "%(qsafe_char)s*" | %(safe_char)s* ' - -param_values_re = re.compile(patterns['param_value_grouped'], re.VERBOSE) -params_re = re.compile(patterns['params_grouped'], re.VERBOSE) -line_re = re.compile(patterns['line'], re.VERBOSE) - -def parseParams(string): - """ - >>> parseParams(';ALTREP="http://www.wiz.org"') - [['ALTREP', 'http://www.wiz.org']] - >>> parseParams('') - [] - >>> parseParams(';ALTREP="http://www.wiz.org;;",Blah,Foo;NEXT=Nope;BAR') - [['ALTREP', 'http://www.wiz.org;;', 'Blah', 'Foo'], ['NEXT', 'Nope'], ['BAR']] - """ - all = params_re.findall(string) - allParameters = [] - for tup in all: - paramList = [tup[0]] # tup looks like (name, valuesString) - for pair in param_values_re.findall(tup[1]): - # pair looks like ('', value) or (value, '') - if pair[0] != '': - paramList.append(pair[0]) - else: - paramList.append(pair[1]) - allParameters.append(paramList) - return allParameters - - -def parseLine(line, lineNumber = None): - """ - >>> parseLine("BLAH:") - ('BLAH', [], '', None) - >>> parseLine("RDATE:VALUE=DATE:19970304,19970504,19970704,19970904") - ('RDATE', [], 'VALUE=DATE:19970304,19970504,19970704,19970904', None) - >>> parseLine('DESCRIPTION;ALTREP="http://www.wiz.org":The Fall 98 Wild Wizards Conference - - Las Vegas, NV, USA') - ('DESCRIPTION', [['ALTREP', 'http://www.wiz.org']], 'The Fall 98 Wild Wizards Conference - - Las Vegas, NV, USA', None) - >>> parseLine("EMAIL;PREF;INTERNET:john@nowhere.com") - ('EMAIL', [['PREF'], ['INTERNET']], 'john@nowhere.com', None) - >>> parseLine('EMAIL;TYPE="blah",hah;INTERNET="DIGI",DERIDOO:john@nowhere.com') - ('EMAIL', [['TYPE', 'blah', 'hah'], ['INTERNET', 'DIGI', 'DERIDOO']], 'john@nowhere.com', None) - >>> parseLine('item1.ADR;type=HOME;type=pref:;;Reeperbahn 116;Hamburg;;20359;') - ('ADR', [['type', 'HOME'], ['type', 'pref']], ';;Reeperbahn 116;Hamburg;;20359;', 'item1') - >>> parseLine(":") - Traceback (most recent call last): - ... - ParseError: 'Failed to parse line: :' - """ - - match = line_re.match(line) - if match is None: - raise ParseError("Failed to parse line: %s" % line, lineNumber) - return (match.group('name'), - parseParams(match.group('params')), - match.group('value'), match.group('group')) - -# logical line regular expressions - -patterns['lineend'] = r'(?:\r\n|\r|\n|$)' -patterns['wrap'] = r'%(lineend)s [\t ]' % patterns -patterns['logicallines'] = r""" -( - (?: [^\r\n] | %(wrap)s )+ - %(lineend)s -) -""" % patterns - -patterns['wraporend'] = r'(%(wrap)s | %(lineend)s )' % patterns - -wrap_re = re.compile(patterns['wraporend'], re.VERBOSE) -logical_lines_re = re.compile(patterns['logicallines'], re.VERBOSE) - -testLines=""" -Line 0 text - , Line 0 continued. -Line 1;encoding=quoted-printable:this is an evil= - evil= - format. -Line 2 is a new line, it does not start with whitespace. -""" - -def getLogicalLines(fp, allowQP=True): - """Iterate through a stream, yielding one logical line at a time. - - Because many applications still use vCard 2.1, we have to deal with the - quoted-printable encoding for long lines, as well as the vCard 3.0 and - vCalendar line folding technique, a whitespace character at the start - of the line. - - Quoted-printable data will be decoded in the Behavior decoding phase. - - >>> import StringIO - >>> f=StringIO.StringIO(testLines) - >>> for n, l in enumerate(getLogicalLines(f)): - ... print "Line %s: %s" % (n, l[0]) - ... - Line 0: Line 0 text, Line 0 continued. - Line 1: Line 1;encoding=quoted-printable:this is an evil= - evil= - format. - Line 2: Line 2 is a new line, it does not start with whitespace. - - """ - if not allowQP: - val = fp.read(-1) - lineNumber = 1 - for match in logical_lines_re.finditer(val): - line, n = wrap_re.subn('', match.group()) - if line != '': - if type(line[0]) != str: - line = line.decode('utf-8') - yield line, lineNumber - lineNumber += n - - else: - quotedPrintable=False - newbuffer = io.StringIO - logicalLine = newbuffer() - lineNumber = 0 - lineStartNumber = 0 - while True: - line = fp.readline() - if line == '': - break - else: - if type(line[0]) != str: - line = line.decode('utf-8') - line = line.rstrip(CRLF) - lineNumber += 1 - if line.rstrip() == '': - if logicalLine.pos > 0: - yield logicalLine.getvalue(), lineStartNumber - lineStartNumber = lineNumber - logicalLine = newbuffer() - quotedPrintable=False - continue - - if quotedPrintable and allowQP: - logicalLine.write('\n') - logicalLine.write(line) - quotedPrintable=False - elif line[0] in SPACEORTAB: - logicalLine.write(line[1:]) - elif logicalLine.pos > 0: - yield logicalLine.getvalue(), lineStartNumber - lineStartNumber = lineNumber - logicalLine = newbuffer() - logicalLine.write(line) - else: - logicalLine = newbuffer() - logicalLine.write(line) - - # hack to deal with the fact that vCard 2.1 allows parameters to be - # encoded without a parameter name. False positives are unlikely, but - # possible. - val = logicalLine.getvalue() - if val[-1]=='=' and val.lower().find('quoted-printable') >= 0: - quotedPrintable=True - - if logicalLine.pos > 0: - yield logicalLine.getvalue(), lineStartNumber - - -def textLineToContentLine(text, n=None): - return ContentLine(*parseLine(text, n), **{'encoded':True, 'lineNumber' : n}) - -def dquoteEscape(param): - """Return param, or "param" if ',' or ';' or ':' is in param.""" - if param.find('"') >= 0: - raise VObjectError("Double quotes aren't allowed in parameter values.") - for char in ',;:': - if param.find(char) >= 0: - return '"'+ param + '"' - return param - -def foldOneLine(outbuf, input, lineLength = 75): - if type(input) in (str, str): input = io.StringIO(input) - input.seek(0) - outbuf.write(input.read(lineLength) + CRLF) - brokenline = input.read(lineLength - 1) - while brokenline: - outbuf.write(' ' + brokenline + CRLF) - brokenline = input.read(lineLength - 1) - -def defaultSerialize(obj, buf, lineLength): - """Encode and fold obj and its children, write to buf or return a string.""" - - if buf: outbuf = buf - else: outbuf=io.StringIO() - - if isinstance(obj, Component): - if obj.group is None: - groupString = '' - else: - groupString = obj.group + '.' - if obj.useBegin: - foldOneLine(outbuf, groupString + "BEGIN:" + obj.name, lineLength) - for child in obj.getSortedChildren(): - #validate is recursive, we only need to validate once - child.serialize(outbuf, lineLength, validate=False) - if obj.useBegin: - foldOneLine(outbuf, groupString + "END:" + obj.name, lineLength) - if DEBUG: logger.debug("Finished %s" % obj.name.upper()) - - elif isinstance(obj, ContentLine): - startedEncoded = obj.encoded - #TODO: X- lines should be considered TEXT, and should be encoded as such - if obj.behavior and not startedEncoded: obj.behavior.encode(obj) - s=io.StringIO() #unfolded buffer - if obj.group is not None: - s.write(obj.group + '.') - if DEBUG: logger.debug("Serializing line" + str(obj)) - s.write(obj.name.upper()) - for key, paramvals in obj.params.items(): - s.write(';' + key + '=' + ','.join(map(dquoteEscape, paramvals))) - s.write(':' + obj.value) - if obj.behavior and not startedEncoded: obj.behavior.decode(obj) - foldOneLine(outbuf, s, lineLength) - if DEBUG: logger.debug("Finished %s line" % obj.name.upper()) - if not buf: - return outbuf.getvalue() - - -testVCalendar=""" -BEGIN:VCALENDAR -BEGIN:VEVENT -SUMMARY;blah=hi!:Bastille Day Party -END:VEVENT -END:VCALENDAR""" - -class Stack: - def __init__(self): - self.stack = [] - def __len__(self): - return len(self.stack) - def top(self): - if len(self) == 0: return None - else: return self.stack[-1] - def topName(self): - if len(self) == 0: return None - else: return self.stack[-1].name - def modifyTop(self, item): - top = self.top() - if top: - top.add(item) - else: - new = Component() - self.push(new) - new.add(item) #add sets behavior for item and children - def push(self, obj): self.stack.append(obj) - def pop(self): return self.stack.pop() - -def readComponents(streamOrString, validate=False, transform=True): - """Generate one Component at a time from a stream. - - >>> import StringIO - >>> f = StringIO.StringIO(testVCalendar) - >>> cal=readComponents(f).next() - >>> cal - ]>]> - >>> cal.vevent.summary - - - """ - if isinstance(streamOrString, str): - stream = io.StringIO(streamOrString) - else: - stream = streamOrString - stack = Stack() - versionLine = None - n = 0 - for line, n in getLogicalLines(stream, False): # not allowing vCard 2.1 - vline = textLineToContentLine(line, n) - if vline.name == "VERSION": - versionLine = vline - stack.modifyTop(vline) - elif vline.name == "BEGIN": - stack.push(Component(vline.value, group=vline.group)) - elif vline.name == "PROFILE": - if not stack.top(): stack.push(Component()) - stack.top().setProfile(vline.value) - elif vline.name == "END": - if len(stack) == 0: - err = "Attempted to end the %s component, \ - but it was never opened" % vline.value - raise ParseError(err, n) - if vline.value.upper() == stack.topName(): #START matches END - if len(stack) == 1: - component=stack.pop() - if versionLine is not None: - component.setBehaviorFromVersionLine(versionLine) - if validate: component.validate(raiseException=True) - if transform: component.transformChildrenToNative() - yield component #EXIT POINT - else: stack.modifyTop(stack.pop()) - else: - err = "%s component wasn't closed" - raise ParseError(err % stack.topName(), n) - else: stack.modifyTop(vline) #not a START or END line - if stack.top(): - if stack.topName() is None: - logger.warning("Top level component was never named") - elif stack.top().useBegin: - raise ParseError("Component %s was never closed" % (stack.topName()), n) - yield stack.pop() - - -def readOne(stream, validate=False, transform=True): - """Return the first component from stream.""" - return next(readComponents(stream, validate, transform)) - -#--------------------------- version registry ---------------------------------- -__behaviorRegistry={} - -def registerBehavior(behavior, name=None, default=False, id=None): - """Register the given behavior. - - If default is True (or if this is the first version registered with this - name), the version will be the default if no id is given. - - """ - if not name: name=behavior.name.upper() - if id is None: id=behavior.versionString - if name in __behaviorRegistry: - if default: - __behaviorRegistry[name].insert(0, (id, behavior)) - else: - __behaviorRegistry[name].append((id, behavior)) - else: - __behaviorRegistry[name]=[(id, behavior)] - -def getBehavior(name, id=None): - """Return a matching behavior if it exists, or None. - - If id is None, return the default for name. - - """ - name=name.upper() - if name in __behaviorRegistry: - if id: - for n, behavior in __behaviorRegistry[name]: - if n==id: - return behavior - else: - return __behaviorRegistry[name][0][1] - return None - -def newFromBehavior(name, id=None): - """Given a name, return a behaviored ContentLine or Component.""" - name = name.upper() - behavior = getBehavior(name, id) - if behavior is None: - raise VObjectError("No behavior found named %s" % name) - if behavior.isComponent: - obj = Component(name) - else: - obj = ContentLine(name, [], '') - obj.behavior = behavior - obj.isNative = True - return obj - - -#--------------------------- Helper function ----------------------------------- -def backslashEscape(s): - s=s.replace("\\","\\\\").replace(";","\;").replace(",","\,") - return s.replace("\r\n", "\\n").replace("\n","\\n").replace("\r","\\n") - -#------------------- Testing and running functions ----------------------------- -if __name__ == '__main__': - from . import tests - tests._test() diff --git a/src/libs/python/bongo/external/vobject/behavior.py b/src/libs/python/bongo/external/vobject/behavior.py deleted file mode 100644 index 7195556..0000000 --- a/src/libs/python/bongo/external/vobject/behavior.py +++ /dev/null @@ -1,143 +0,0 @@ -"""Behavior (validation, encoding, and transformations) for vobjects.""" - -from . import base - -#------------------------ Abstract class for behavior -------------------------- -class Behavior(object): - """Abstract class to describe vobject options, requirements and encodings. - - Behaviors are used for root components like VCALENDAR, for subcomponents - like VEVENT, and for individual lines in components. - - Behavior subclasses are not meant to be instantiated, all methods should - be classmethods. - - @cvar name: - The uppercase name of the object described by the class, or a generic - name if the class defines behavior for many objects. - @cvar description: - A brief excerpt from the RFC explaining the function of the component or - line. - @cvar versionString: - The string associated with the component, for instance, 2.0 if there's a - line like VERSION:2.0, an empty string otherwise. - @cvar knownChildren: - A dictionary with uppercased component/property names as keys and a - tuple (min, max, id) as value, where id is the id used by - L{registerBehavior}, min and max are the limits on how many of this child - must occur. None is used to denote no max or no id. - @cvar quotedPrintable: - A boolean describing whether the object should be encoded and decoded - using quoted printable line folding and character escaping. - @cvar hasNative: - A boolean describing whether the object can be transformed into a more - Pythonic object. - @cvar isComponent: - A boolean, True if the object should be a Component. - @cvar sortFirst: - The lower-case list of children which should come first when sorting. - @cvar allowGroup: - Whether or not vCard style group prefixes are allowed. - """ - name='' - description='' - versionString='' - knownChildren = {} - quotedPrintable = False - hasNative= False - isComponent = False - allowGroup = False - sortFirst = [] - - def __init__(self): - err="Behavior subclasses are not meant to be instantiated" - raise base.VObjectError(err) - - def validate(cls, obj, raiseException=False, complainUnrecognized=False): - """Check if the object satisfies this behavior's requirements. - - @param obj: - The L{ContentLine} or - L{Component} to be validated. - @param raiseException: - If True, raise a L{base.ValidateError} on validation failure. - Otherwise return a boolean. - @param complainUnrecognized: - If True, fail to validate if an uncrecognized parameter or child is - found. Otherwise log the lack of recognition. - - """ - if not cls.allowGroup and obj.group is not None: - err = str(obj) + " has a group, but this object doesn't support groups" - raise base.VObjectError(err) - if isinstance(obj, base.ContentLine): - return cls.lineValidate(obj, raiseException, complainUnrecognized) - elif isinstance(obj, base.Component): - count = {} - for child in obj.getChildren(): - if not child.validate(raiseException, complainUnrecognized): - return False - name=child.name.upper() - count[name] = count.get(name, 0) + 1 - for key, val in cls.knownChildren.items(): - if count.get(key,0) < val[0]: - if raiseException: - m = "%s components must contain at least %i %s" - raise base.ValidateError(m % (cls.name, val[0], key)) - return False - if val[1] and count.get(key,0) > val[1]: - if raiseException: - m = "%s components cannot contain more than %i %s" - raise base.ValidateError(m % (cls.name, val[1], key)) - return False - return True - else: - err = str(obj) + " is not a Component or Contentline" - raise base.VObjectError(err) - - def lineValidate(cls, line, raiseException, complainUnrecognized): - """Examine a line's parameters and values, return True if valid.""" - return True - - def decode(cls, line): - if line.encoded: line.encoded=0 - - def encode(cls, line): - if not line.encoded: line.encoded=1 - - def transformToNative(cls, obj): - """Turn a ContentLine or Component into a Python-native representation. - - If appropriate, turn dates or datetime strings into Python objects. - Components containing VTIMEZONEs turn into VtimezoneComponents. - - """ - return obj - - def transformFromNative(cls, obj): - """Inverse of transformToNative.""" - raise base.NativeError("No transformFromNative defined") - - def generateImplicitParameters(cls, obj): - """Generate any required information that don't yet exist.""" - pass - - def serialize(cls, obj, buf, lineLength): - """Set implicit parameters, do encoding, return unicode string. - - If validate is True, raise VObjectError if the line doesn't validate - after implicit parameters are generated. - - Default is to call base.defaultSerialize. - - """ - return base.defaultSerialize(obj, buf, lineLength) - - lineValidate = classmethod(lineValidate) - validate = classmethod(validate) - decode = classmethod(decode) - encode = classmethod(encode) - serialize = classmethod(serialize) - transformToNative = classmethod(transformToNative) - transformFromNative = classmethod(transformFromNative) - generateImplicitParameters = classmethod(generateImplicitParameters) \ No newline at end of file diff --git a/src/libs/python/bongo/external/vobject/icalendar.py b/src/libs/python/bongo/external/vobject/icalendar.py deleted file mode 100644 index 923d65b..0000000 --- a/src/libs/python/bongo/external/vobject/icalendar.py +++ /dev/null @@ -1,1661 +0,0 @@ -"""Definitions and behavior for iCalendar, also known as vCalendar 2.0""" - -# generator support for Python 2.3 - - -import string -from . import behavior -import bongo.external.dateutil.rrule as rrule -import bongo.external.dateutil.tz as tz -import io -import datetime -import socket, random #for generating a UID -import itertools - -from .base import VObjectError, NativeError, ValidateError, ParseError, \ - VBase, Component, ContentLine, logger, defaultSerialize, \ - registerBehavior, backslashEscape, foldOneLine, \ - newFromBehavior, CRLF, LF - -#------------------------------- Constants ------------------------------------- -DATENAMES = ("rdate", "exdate") -RULENAMES = ("exrule", "rrule") -DATESANDRULES = ("exrule", "rrule", "rdate", "exdate") -PRODID = "-//PYVOBJECT//NONSGML Version 1//EN" - -WEEKDAYS = "MO", "TU", "WE", "TH", "FR", "SA", "SU" -FREQUENCIES = ('YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY', - 'SECONDLY') - -zeroDelta = datetime.timedelta(0) -twoHours = datetime.timedelta(hours=2) - -#---------------------------- TZID registry ------------------------------------ -__tzidMap={} - -def registerTzid(tzid, tzinfo): - """Register a tzid -> tzinfo mapping.""" - __tzidMap[tzid]=tzinfo - -def getTzid(tzid): - """Return the tzid if it exists, or None.""" - return __tzidMap.get(tzid, None) - -utc = tz.tzutc() -registerTzid("UTC", utc) - -#-------------------- Helper subclasses ---------------------------------------- - -class TimezoneComponent(Component): - """A VTIMEZONE object. - - VTIMEZONEs are parsed by dateutil.tz.tzical, the resulting datetime.tzinfo - subclass is stored in self.tzinfo, self.tzid stores the TZID associated - with this timezone. - - @ivar name: - The uppercased name of the object, in this case always 'VTIMEZONE'. - @ivar tzinfo: - A datetime.tzinfo subclass representing this timezone. - @ivar tzid: - The string used to refer to this timezone. - - """ - def __init__(self, tzinfo=None, *args, **kwds): - """Accept an existing Component or a tzinfo class.""" - super(TimezoneComponent, self).__init__(*args, **kwds) - self.isNative=True - # hack to make sure a behavior is assigned - if self.behavior is None: - self.behavior = VTimezone - if tzinfo is not None: - self.tzinfo = tzinfo - if not hasattr(self, 'name') or self.name == '': - self.name = 'VTIMEZONE' - self.useBegin = True - - def registerTzinfo(obj, tzinfo): - """Register tzinfo if it's not already registered, return its tzid.""" - tzid = obj.pickTzid(tzinfo) - if tzid and not getTzid(tzid): - registerTzid(tzid, tzinfo) - return tzid - registerTzinfo = classmethod(registerTzinfo) - - def gettzinfo(self): - # workaround for dateutil failing to parse some experimental properties - good_lines = ('rdate', 'rrule', 'dtstart', 'tzname', 'tzoffsetfrom', - 'tzoffsetto', 'tzid') - buffer = io.StringIO() - def customSerialize(obj): - if isinstance(obj, Component): - foldOneLine(buffer, "BEGIN:" + obj.name) - for child in obj.lines(): - if child.name.lower() in good_lines: - child.serialize(buffer, 75, validate=False) - for comp in obj.components(): - customSerialize(comp) - foldOneLine(buffer, "END:" + obj.name) - customSerialize(self) - return tz.tzical(io.StringIO(str(buffer.getvalue()))).get() - - def settzinfo(self, tzinfo, start=2000, end=2030): - """Create appropriate objects in self to represent tzinfo. - - Collapse DST transitions to rrules as much as possible. - - Assumptions: - - DST <-> Standard transitions occur on the hour - - never within a month of one another - - twice or fewer times a year - - never in the month of December - - DST always moves offset exactly one hour later - - tzinfo classes dst method always treats times that could be in either - offset as being in the later regime - - """ - def fromLastWeek(dt): - """How many weeks from the end of the month dt is, starting from 1.""" - weekDelta = datetime.timedelta(weeks=1) - n = 1 - current = dt + weekDelta - while current.month == dt.month: - n += 1 - current += weekDelta - return n - - # lists of dictionaries defining rules which are no longer in effect - completed = {'daylight' : [], 'standard' : []} - - # dictionary defining rules which are currently in effect - working = {'daylight' : None, 'standard' : None} - - # rule may be based on the nth week of the month or the nth from the last - for year in range(start, end + 1): - newyear = datetime.datetime(year, 1, 1) - for transitionTo in 'daylight', 'standard': - transition = getTransition(transitionTo, year, tzinfo) - oldrule = working[transitionTo] - - if transition == newyear: - # transitionTo is in effect for the whole year - rule = {'end' : None, - 'start' : newyear, - 'month' : 1, - 'weekday' : None, - 'hour' : None, - 'plus' : None, - 'minus' : None, - 'name' : tzinfo.tzname(newyear), - 'offset' : tzinfo.utcoffset(newyear), - 'offsetfrom' : tzinfo.utcoffset(newyear)} - if oldrule is None: - # transitionTo was not yet in effect - working[transitionTo] = rule - else: - # transitionTo was already in effect - if (oldrule['offset'] != - tzinfo.utcoffset(newyear)): - # old rule was different, it shouldn't continue - oldrule['end'] = year - 1 - completed[transitionTo].append(oldrule) - working[transitionTo] = rule - elif transition is None: - # transitionTo is not in effect - if oldrule is not None: - # transitionTo used to be in effect - oldrule['end'] = year - 1 - completed[transitionTo].append(oldrule) - working[transitionTo] = None - else: - # an offset transition was found - old_offset = tzinfo.utcoffset(transition - twoHours) - rule = {'end' : None, # None, or an integer year - 'start' : transition, # the datetime of transition - 'month' : transition.month, - 'weekday' : transition.weekday(), - 'hour' : transition.hour, - 'name' : tzinfo.tzname(transition), - 'plus' : (transition.day - 1)/ 7 + 1,#nth week of the month - 'minus' : fromLastWeek(transition), #nth from last week - 'offset' : tzinfo.utcoffset(transition), - 'offsetfrom' : old_offset} - - if oldrule is None: - working[transitionTo] = rule - else: - plusMatch = rule['plus'] == oldrule['plus'] - minusMatch = rule['minus'] == oldrule['minus'] - truth = plusMatch or minusMatch - for key in 'month', 'weekday', 'hour', 'offset': - truth = truth and rule[key] == oldrule[key] - if truth: - # the old rule is still true, limit to plus or minus - if not plusMatch: - oldrule['plus'] = None - if not minusMatch: - oldrule['minus'] = None - else: - # the new rule did not match the old - oldrule['end'] = year - 1 - completed[transitionTo].append(oldrule) - working[transitionTo] = rule - - for transitionTo in 'daylight', 'standard': - if working[transitionTo] is not None: - completed[transitionTo].append(working[transitionTo]) - - self.tzid = [] - self.daylight = [] - self.standard = [] - - self.add('tzid').value = self.pickTzid(tzinfo) - - old = None - for transitionTo in 'daylight', 'standard': - for rule in completed[transitionTo]: - comp = self.add(transitionTo) - dtstart = comp.add('dtstart') - dtstart.value = rule['start'] - comp.add('tzname').value = rule['name'] - line = comp.add('tzoffsetto') - line.value = deltaToOffset(rule['offset']) - line = comp.add('tzoffsetfrom') - line.value = deltaToOffset(rule['offsetfrom']) - - if rule['plus'] is not None: - num = rule['plus'] - elif rule['minus'] is not None: - num = -1 * rule['minus'] - else: - num = None - if num is not None: - dayString = ";BYDAY=" + str(num) + WEEKDAYS[rule['weekday']] - else: - dayString = "" - if rule['end'] is not None: - if rule['hour'] is None: - # all year offset, with no rule - endDate = datetime.datetime(rule['end'], 1, 1) - else: - weekday = rrule.weekday(rule['weekday'], num) - du_rule = rrule.rrule(rrule.YEARLY, - bymonth = rule['month'],byweekday = weekday, - dtstart = datetime.datetime( - rule['end'], 1, 1, rule['hour']) - ) - endDate = du_rule[0] - endDate = endDate.replace(tzinfo = utc) - rule['offsetfrom'] - endString = ";UNTIL="+ dateTimeToString(endDate) - else: - endString = '' - rulestring = "FREQ=YEARLY%s;BYMONTH=%s%s" % \ - (dayString, str(rule['month']), endString) - - comp.add('rrule').value = rulestring - - tzinfo = property(gettzinfo, settzinfo) - # prevent Component's __setattr__ from overriding the tzinfo property - normal_attributes = Component.normal_attributes + ['tzinfo'] - - def pickTzid(tzinfo): - """ - Given a tzinfo class, use known APIs to determine TZID, or use tzname. - """ - if tzinfo is None or tzinfo_eq(tzinfo, utc): - #If tzinfo is UTC, we don't need a TZID - return None - # try PyICU's tzid key - if hasattr(tzinfo, 'tzid'): - return tzinfo.tzid - - # try tzical's tzid key - elif hasattr(tzinfo, '_tzid'): - return tzinfo._tzid - else: - # return tzname for standard (non-DST) time - notDST = datetime.timedelta(0) - for month in range(1,13): - dt = datetime.datetime(2000, month, 1) - if tzinfo.dst(dt) == notDST: - return tzinfo.tzname(dt) - # there was no standard time in 2000! - raise VObjectError("Unable to guess TZID for tzinfo %s" % str(tzinfo)) - pickTzid = staticmethod(pickTzid) - - def __str__(self): - return "" - - def __repr__(self): - return self.__str__() - - def prettyPrint(self, level, tabwidth): - pre = ' ' * level * tabwidth - print(pre, self.name) - print(pre, "TZID:", self.tzid) - print() - -class RecurringComponent(Component): - """A vCalendar component like VEVENT or VTODO which may recur. - - Any recurring component can have one or multiple RRULE, RDATE, - EXRULE, or EXDATE lines, and one or zero DTSTART lines. It can also have a - variety of children that don't have any recurrence information. - - In the example below, note that dtstart is included in the rruleset. - This is not the default behavior for dateutil's rrule implementation unless - dtstart would already have been a member of the recurrence rule, and as a - result, COUNT is wrong. This can be worked around when getting rruleset by - adjusting count down by one if an rrule has a count and dtstart isn't in its - result set, but by default, the rruleset property doesn't do this work - around, to access it getrruleset must be called with addRDate set True. - - >>> import dateutil.rrule, datetime - >>> vevent = RecurringComponent(name='VEVENT') - >>> vevent.add('rrule').value =u"FREQ=WEEKLY;COUNT=2;INTERVAL=2;BYDAY=TU,TH" - >>> vevent.add('dtstart').value = datetime.datetime(2005, 1, 19, 9) - - When creating rrule's programmatically it should be kept in - mind that count doesn't necessarily mean what rfc2445 says. - - >>> list(vevent.rruleset) - [datetime.datetime(2005, 1, 20, 9, 0), datetime.datetime(2005, 2, 1, 9, 0)] - >>> list(vevent.getrruleset(addRDate=True)) - [datetime.datetime(2005, 1, 19, 9, 0), datetime.datetime(2005, 1, 20, 9, 0)] - - Also note that dateutil will expand all-day events (datetime.date values) to - datetime.datetime value with time 0 and no timezone. - - >>> vevent.dtstart.value = datetime.date(2005,3,18) - >>> list(vevent.rruleset) - [datetime.datetime(2005, 3, 29, 0, 0), datetime.datetime(2005, 3, 31, 0, 0)] - >>> list(vevent.getrruleset(True)) - [datetime.datetime(2005, 3, 18, 0, 0), datetime.datetime(2005, 3, 29, 0, 0)] - - @ivar rruleset: - A U{rruleset}. - """ - def __init__(self, *args, **kwds): - super(RecurringComponent, self).__init__(*args, **kwds) - self.isNative=True - #self.clobberedRDates=[] - - - def getrruleset(self, addRDate = False): - """Get an rruleset created from self. - - If addRDate is True, add an RDATE for dtstart if it's not included in - an RRULE, and count is decremented if it exists. - - Note that for rules which don't match DTSTART, DTSTART may not appear - in list(rruleset), although it should. By default, an RDATE is not - created in these cases, and count isn't updated, so dateutil may list - a spurious occurrence. - - """ - rruleset = None - for name in DATESANDRULES: - addfunc = None - for line in self.contents.get(name, ()): - # don't bother creating a rruleset unless there's a rule - if rruleset is None: - rruleset = rrule.rruleset() - if addfunc is None: - addfunc=getattr(rruleset, name) - - if name in DATENAMES: - if type(line.value[0]) == datetime.datetime: - list(map(addfunc, line.value)) - elif type(line.value) == datetime.date: - for dt in line.value: - addfunc(datetime.datetime(dt.year, dt.month, dt.day)) - else: - # ignore RDATEs with PERIOD values for now - pass - elif name in RULENAMES: - try: - dtstart = self.dtstart.value - except AttributeError as KeyError: - # if there's no dtstart, just return None - return None - # rrulestr complains about unicode, so cast to str - addfunc(rrule.rrulestr(str(line.value), - dtstart=dtstart)) - if name == 'rrule' and addRDate: - try: - # dateutils does not work with all-day (datetime.date) items - # so we need to convert to a datetime.datetime - # (which is what dateutils does internally) - if not isinstance(dtstart, datetime.datetime): - adddtstart = datetime.datetime.fromordinal(dtstart.toordinal()) - else: - adddtstart = dtstart - if rruleset._rrule[-1][0] != adddtstart: - rruleset.rdate(adddtstart) - added = True - else: - added = False - except IndexError: - # it's conceivable that an rrule might have 0 datetimes - added = False - if added and rruleset._rrule[-1]._count != None: - rruleset._rrule[-1]._count -= 1 - return rruleset - - def setrruleset(self, rruleset): - dtstart = self.dtstart.value - isDate = datetime.date == type(dtstart) - if isDate: - dtstart = datetime.datetime(dtstart.year,dtstart.month, dtstart.day) - untilSerialize = dateToString - else: - # make sure to convert time zones to UTC - untilSerialize = lambda x: dateTimeToString(x, True) - - for name in DATESANDRULES: - if hasattr(self.contents, name): - del self.contents[name] - setlist = getattr(rruleset, '_' + name) - if name in DATENAMES: - setlist = list(setlist) # make a copy of the list - if name == 'rdate' and dtstart in setlist: - setlist.remove(dtstart) - if isDate: - setlist = [dt.date() for dt in setlist] - if len(setlist) > 0: - self.add(name).value = setlist - elif name in RULENAMES: - for rule in setlist: - buf = io.StringIO() - buf.write('FREQ=') - buf.write(FREQUENCIES[rule._freq]) - - values = {} - - if rule._interval != 1: - values['INTERVAL'] = [str(rule._interval)] - if rule._wkst != 0: # wkst defaults to Monday - values['WKST'] = [WEEKDAYS[rule._wkst]] - if rule._bysetpos is not None: - values['BYSETPOS'] = [str(i) for i in rule._bysetpos] - - if rule._count is not None: - values['COUNT'] = [str(rule._count)] - elif rule._until is not None: - values['UNTIL'] = [untilSerialize(rule._until)] - - days = [] - if (rule._byweekday is not None and ( - rrule.WEEKLY != rule._freq or - len(rule._byweekday) != 1 or - rule._dtstart.weekday() != rule._byweekday[0])): - # ignore byweekday if freq is WEEKLY and day correlates - # with dtstart because it was automatically set by - # dateutil - days.extend([WEEKDAYS[n] for n in rule._byweekday]) - - if rule._bynweekday is not None: - days.extend([str(n) + WEEKDAYS[day] for day, n in rule._bynweekday]) - - if len(days) > 0: - values['BYDAY'] = days - - if rule._bymonthday is not None and len(rule._bymonthday) > 0: - if not (rule._freq <= rrule.MONTHLY and - len(rule._bymonthday) == 1 and - rule._bymonthday[0] == rule._dtstart.day): - # ignore bymonthday if it's generated by dateutil - values['BYMONTHDAY'] = [str(n) for n in rule._bymonthday] - - if rule._bymonth is not None and len(rule._bymonth) > 0: - if not (rule._freq == rrule.YEARLY and - len(rule._bymonth) == 1 and - rule._bymonth[0] == rule._dtstart.month): - # ignore bymonth if it's generated by dateutil - values['BYMONTH'] = [str(n) for n in rule._bymonth] - - if rule._byyearday is not None: - values['BYYEARDAY'] = [str(n) for n in rule._byyearday] - if rule._byweekno is not None: - values['BYWEEKNO'] = [str(n) for n in rule._byweekno] - - # byhour, byminute, bysecond are always ignored for now - - - for key, paramvals in values.items(): - buf.write(';') - buf.write(key) - buf.write('=') - buf.write(','.join(paramvals)) - - self.add(name).value = buf.getvalue() - - - - rruleset = property(getrruleset, setrruleset) - - def __setattr__(self, name, value): - """For convenience, make self.contents directly accessible.""" - if name == 'rruleset': - self.setrruleset(value) - else: - super(RecurringComponent, self).__setattr__(name, value) - -class RecurringBehavior(behavior.Behavior): - """Parent Behavior for components which should be RecurringComponents.""" - hasNative = True - isComponent = True - - def transformToNative(obj): - """Turn a recurring Component into a RecurringComponent.""" - if not obj.isNative: - object.__setattr__(obj, '__class__', RecurringComponent) - obj.isNative = True - return obj - transformToNative = staticmethod(transformToNative) - - def transformFromNative(obj): - if obj.isNative: - object.__setattr__(obj, '__class__', Component) - obj.isNative = False - return obj - transformFromNative = staticmethod(transformFromNative) - - def generateImplicitParameters(obj): - """Generate a UID if one does not exist. - - This is just a dummy implementation, for now. - - """ - if not hasattr(obj, 'uid'): - rand = str(int(random.random() * 100000)) - now = datetime.datetime.now(utc) - now = dateTimeToString(now) - host = socket.gethostname() - obj.add(ContentLine('UID', [], now + '-' + rand + '@' + host)) - generateImplicitParameters = staticmethod(generateImplicitParameters) - - - -class DateTimeBehavior(behavior.Behavior): - """Parent Behavior for ContentLines containing one DATE-TIME.""" - hasNative = True - - def transformToNative(obj): - """Turn obj.value into a datetime. - - RFC2445 allows times without time zone information, "floating times" - in some properties. Mostly, this isn't what you want, but when parsing - a file, real floating times are noted by setting to 'TRUE' the - X-VOBJ-FLOATINGTIME-ALLOWED parameter. - - """ - if obj.isNative: return obj - obj.isNative = True - obj.value=str(obj.value) - #we're cheating a little here, parseDtstart allows DATE - obj.value=parseDtstart(obj) - if obj.value.tzinfo is None: - obj.params['X-VOBJ-FLOATINGTIME-ALLOWED'] = ['TRUE'] - if obj.params.get('TZID'): - del obj.params['TZID'] - return obj - transformToNative = staticmethod(transformToNative) - - def transformFromNative(obj, convertToUTC = False): - """Replace the datetime in obj.value with an ISO 8601 string.""" - if obj.isNative: - obj.isNative = False - tzid = TimezoneComponent.registerTzinfo(obj.value.tzinfo) - obj.value = dateTimeToString(obj.value, convertToUTC) - if not convertToUTC and tzid is not None: - obj.tzid_param = tzid - - return obj - transformFromNative = staticmethod(transformFromNative) - -class UTCDateTimeBehavior(DateTimeBehavior): - """A value which must be specified in UTC.""" - - def transformFromNative(obj): - return DateTimeBehavior.transformFromNative(obj, False) - transformFromNative = staticmethod(transformFromNative) - -class DateOrDateTimeBehavior(behavior.Behavior): - """Parent Behavior for ContentLines containing one DATE or DATE-TIME.""" - hasNative = True - - def transformToNative(obj): - """Turn obj.value into a date or datetime.""" - if obj.isNative: return obj - obj.isNative = True - if obj.value == '': return obj - obj.value=str(obj.value) - obj.value=parseDtstart(obj) - if getattr(obj, 'value_param', 'DATE-TIME').upper() == 'DATE-TIME': - if hasattr(obj, 'tzid_param'): del obj.tzid_param - return obj - transformToNative = staticmethod(transformToNative) - - def transformFromNative(obj): - """Replace the date or datetime in obj.value with an ISO 8601 string.""" - if type(obj.value) == datetime.date: - obj.isNative = False - obj.value_param = 'DATE' - obj.value = dateToString(obj.value) - return obj - else: return DateTimeBehavior.transformFromNative(obj) - transformFromNative = staticmethod(transformFromNative) - -class MultiDateBehavior(behavior.Behavior): - """ - Parent Behavior for ContentLines containing one or more DATE, DATE-TIME, or - PERIOD. - - """ - hasNative = True - - def transformToNative(obj): - """ - Turn obj.value into a list of dates, datetimes, or - (datetime, timedelta) tuples. - - """ - if obj.isNative: - return obj - obj.isNative = True - if obj.value == '': - obj.value = [] - return obj - tzinfo = getTzid(getattr(obj, 'tzid_param', None)) - valueParam = getattr(obj, 'value_param', "DATE-TIME").upper() - valTexts = obj.value.split(",") - if valueParam == "DATE": - obj.value = [stringToDate(x) for x in valTexts] - elif valueParam == "DATE-TIME": - obj.value = [stringToDateTime(x, tzinfo) for x in valTexts] - elif valueParam == "PERIOD": - obj.value = [stringToPeriod(x, tzinfo) for x in valTexts] - return obj - transformToNative = staticmethod(transformToNative) - - def transformFromNative(obj): - """ - Replace the date, datetime or period tuples in obj.value with - appropriate strings. - - """ - # Fixme: obj.value should be a list, so this test should never succeed - if type(obj.value) == datetime.date: - obj.isNative = False - obj.value_param = 'DATE' - obj.value = ','.join([dateToString(val) for val in obj.value]) - return obj - # Fixme: handle PERIOD case - else: - if obj.isNative: - obj.isNative = False - transformed = [] - tzid = None - for val in obj.value: - if tzid is None and type(val) == datetime.datetime: - tzid = TimezoneComponent.registerTzinfo(val.tzinfo) - if tzid is not None: - obj.tzid_param = tzid - transformed.append(dateTimeToString(val)) - obj.value = ','.join(transformed) - return obj - transformFromNative = staticmethod(transformFromNative) - -class TextBehavior(behavior.Behavior): - """Provide backslash escape encoding/decoding for single valued properties. - - TextBehavior also deals with base64 encoding if the ENCODING parameter is - explicitly set to BASE64. - - """ - base64string = 'BASE64' # vCard uses B - - def decode(cls, line): - """Remove backslash escaping from line.value.""" - if line.encoded: - encoding = getattr(line, 'encoding_param', None) - if encoding and encoding.upper() == cls.base64string: - line.value = line.value.decode('base64') - else: - line.value = stringToTextValues(line.value)[0] - line.encoded=False - decode = classmethod(decode) - - def encode(cls, line): - """Backslash escape line.value.""" - if not line.encoded: - encoding = getattr(line, 'encoding_param', None) - if encoding and encoding.upper() == cls.base64string: - line.value = line.value.encode('base64').replace('\n', '') - else: - line.value = backslashEscape(line.value) - line.encoded=True - encode = classmethod(encode) - -class MultiTextBehavior(behavior.Behavior): - """Provide backslash escape encoding/decoding of each of several values. - - After transformation, value is a list of strings. - - """ - - def decode(line): - """Remove backslash escaping from line.value, then split on commas.""" - if line.encoded: - line.value = stringToTextValues(line.value) - line.encoded=False - decode = staticmethod(decode) - - def encode(line): - """Backslash escape line.value.""" - if not line.encoded: - line.value = ','.join([backslashEscape(val) for val in line.value]) - line.encoded=True - encode = staticmethod(encode) - - -#------------------------ Registered Behavior subclasses ----------------------- -class VCalendar2_0(behavior.Behavior): - """vCalendar 2.0 behavior.""" - name = 'VCALENDAR' - description = 'vCalendar 2.0, also known as iCalendar.' - versionString = '2.0' - isComponent = True - sortFirst = ('version', 'calscale', 'method', 'prodid', 'vtimezone') - knownChildren = {'CALSCALE': (0, 1, None),#min, max, behaviorRegistry id - 'METHOD': (0, 1, None), - 'VERSION': (0, 1, None),#required, but auto-generated - 'PRODID': (1, 1, None), - 'VTIMEZONE': (0, None, None), - 'VEVENT': (0, None, None), - 'VTODO': (0, None, None), - 'VJOURNAL': (0, None, None), - 'VFREEBUSY': (0, None, None) - } - - def generateImplicitParameters(cls, obj): - """Create PRODID, VERSION, and VTIMEZONEs if needed. - - VTIMEZONEs will need to exist whenever TZID parameters exist or when - datetimes with tzinfo exist. - - """ - for comp in obj.components(): - if comp.behavior is not None: - comp.behavior.generateImplicitParameters(comp) - if not hasattr(obj, 'prodid'): - obj.add(ContentLine('PRODID', [], PRODID)) - if not hasattr(obj, 'version'): - obj.add(ContentLine('VERSION', [], cls.versionString)) - tzidsUsed = {} - - def findTzids(obj, table): - if isinstance(obj, ContentLine): - if getattr(obj, 'tzid_param', None): - table[obj.tzid_param] = 1 - else: - if type(obj.value) == list: - for item in obj.value: - tzinfo = getattr(obj.value, 'tzinfo', None) - tzid = TimezoneComponent.registerTzinfo(tzinfo) - if tzid: - table[tzid] = 1 - else: - tzinfo = getattr(obj.value, 'tzinfo', None) - tzid = TimezoneComponent.registerTzinfo(tzinfo) - if tzid: - table[tzid] = 1 - for child in obj.getChildren(): - if obj.name is not 'VTIMEZONE': - findTzids(child, table) - - findTzids(obj, tzidsUsed) - oldtzids = [x.tzid.value for x in getattr(obj, 'vtimezone_list', [])] - for tzid in list(tzidsUsed.keys()): - if tzid == 'UTC' or tzid in oldtzids: continue - obj.add(TimezoneComponent(tzinfo=getTzid(tzid))) - generateImplicitParameters = classmethod(generateImplicitParameters) -registerBehavior(VCalendar2_0) - -class VTimezone(behavior.Behavior): - """Timezone behavior.""" - name = 'VTIMEZONE' - hasNative = True - isComponent = True - description = 'A grouping of component properties that defines a time zone.' - sortFirst = ('tzid', 'last-modified', 'tzurl', 'standard', 'daylight') - knownChildren = {'TZID': (1, 1, None),#min, max, behaviorRegistry id - 'LAST-MODIFIED':(0, 1, None), - 'TZURL': (0, 1, None), - 'STANDARD': (0, None, None),#NOTE: One of Standard or - 'DAYLIGHT': (0, None, None) # Daylight must appear - } - - def validate(cls, obj, raiseException, *args): - return True #TODO: FIXME - if 'standard' in obj.contents or 'daylight' in obj.contents: - return super(VTimezone, cls).validate(obj, raiseException, *args) - else: - if raiseException: - m = "VTIMEZONE components must contain a STANDARD or a DAYLIGHT\ - component" - raise ValidateError(m) - return False - validate = classmethod(validate) - - - def transformToNative(obj): - if not obj.isNative: - object.__setattr__(obj, '__class__', TimezoneComponent) - obj.isNative = True - obj.registerTzinfo(obj.tzinfo) - return obj - transformToNative = staticmethod(transformToNative) - - def transformFromNative(obj): - return obj - transformFromNative = staticmethod(transformFromNative) - - -registerBehavior(VTimezone) - -class DaylightOrStandard(behavior.Behavior): - hasNative = False - isComponent = True - knownChildren = {'DTSTART': (1, 1, None)}#min, max, behaviorRegistry id - -registerBehavior(DaylightOrStandard, 'STANDARD') -registerBehavior(DaylightOrStandard, 'DAYLIGHT') - - -class VEvent(RecurringBehavior): - """Event behavior.""" - name='VEVENT' - sortFirst = ('uid', 'recurrence-id', 'dtstart', 'duration', 'dtend') - - description='A grouping of component properties, and possibly including \ - "VALARM" calendar components, that represents a scheduled \ - amount of time on a calendar.' - knownChildren = {'DTSTART': (0, 1, None),#min, max, behaviorRegistry id - 'CLASS': (0, 1, None), - 'CREATED': (0, 1, None), - 'DESCRIPTION': (0, 1, None), - 'GEO': (0, 1, None), - 'LAST-MODIFIED':(0, 1, None), - 'LOCATION': (0, 1, None), - 'ORGANIZER': (0, 1, None), - 'PRIORITY': (0, 1, None), - 'DTSTAMP': (0, 1, None), - 'SEQUENCE': (0, 1, None), - 'STATUS': (0, 1, None), - 'SUMMARY': (0, 1, None), - 'TRANSP': (0, 1, None), - 'UID': (1, 1, None), - 'URL': (0, 1, None), - 'RECURRENCE-ID':(0, 1, None), - 'DTEND': (0, 1, None), #NOTE: Only one of DtEnd or - 'DURATION': (0, 1, None), # Duration can appear - 'ATTACH': (0, None, None), - 'ATTENDEE': (0, None, None), - 'CATEGORIES': (0, None, None), - 'COMMENT': (0, None, None), - 'CONTACT': (0, None, None), - 'EXDATE': (0, None, None), - 'EXRULE': (0, None, None), - 'REQUEST-STATUS': (0, None, None), - 'RELATED-TO': (0, None, None), - 'RESOURCES': (0, None, None), - 'RDATE': (0, None, None), - 'RRULE': (0, None, None), - 'VALARM': (0, None, None) - } - - def validate(cls, obj, raiseException, *args): - if 'DTEND' in obj.contents and 'DURATION' in obj.contents: - if raiseException: - m = "VEVENT components cannot contain both DTEND and DURATION\ - components" - raise ValidateError(m) - return False - else: - return super(VEvent, cls).validate(obj, raiseException, *args) - validate = classmethod(validate) - -registerBehavior(VEvent) - - -class VTodo(RecurringBehavior): - """To-do behavior.""" - name='VTODO' - description='A grouping of component properties and possibly "VALARM" \ - calendar components that represent an action-item or \ - assignment.' - knownChildren = {'DTSTART': (0, 1, None),#min, max, behaviorRegistry id - 'CLASS': (0, 1, None), - 'COMPLETED': (0, 1, None), - 'CREATED': (0, 1, None), - 'DESCRIPTION': (0, 1, None), - 'GEO': (0, 1, None), - 'LAST-MODIFIED':(0, 1, None), - 'LOCATION': (0, 1, None), - 'ORGANIZER': (0, 1, None), - 'PERCENT': (0, 1, None), - 'PRIORITY': (0, 1, None), - 'DTSTAMP': (0, 1, None), - 'SEQUENCE': (0, 1, None), - 'STATUS': (0, 1, None), - 'SUMMARY': (0, 1, None), - 'UID': (0, 1, None), - 'URL': (0, 1, None), - 'RECURRENCE-ID':(0, 1, None), - 'DUE': (0, 1, None), #NOTE: Only one of Due or - 'DURATION': (0, 1, None), # Duration can appear - 'ATTACH': (0, None, None), - 'ATTENDEE': (0, None, None), - 'CATEGORIES': (0, None, None), - 'COMMENT': (0, None, None), - 'CONTACT': (0, None, None), - 'EXDATE': (0, None, None), - 'EXRULE': (0, None, None), - 'REQUEST-STATUS': (0, None, None), - 'RELATED-TO': (0, None, None), - 'RESOURCES': (0, None, None), - 'RDATE': (0, None, None), - 'RRULE': (0, None, None), - 'VALARM': (0, None, None) - } - - def validate(cls, obj, raiseException, *args): - if 'DUE' in obj.contents and 'DURATION' in obj.contents: - if raiseException: - m = "VTODO components cannot contain both DUE and DURATION\ - components" - raise ValidateError(m) - return False - else: - return super(VTodo, cls).validate(obj, raiseException, *args) - validate = classmethod(validate) - -registerBehavior(VTodo) - - -class VJournal(RecurringBehavior): - """Journal entry behavior.""" - name='VJOURNAL' - knownChildren = {'DTSTART': (0, 1, None),#min, max, behaviorRegistry id - 'CLASS': (0, 1, None), - 'CREATED': (0, 1, None), - 'DESCRIPTION': (0, 1, None), - 'LAST-MODIFIED':(0, 1, None), - 'ORGANIZER': (0, 1, None), - 'DTSTAMP': (0, 1, None), - 'SEQUENCE': (0, 1, None), - 'STATUS': (0, 1, None), - 'SUMMARY': (0, 1, None), - 'UID': (0, 1, None), - 'URL': (0, 1, None), - 'RECURRENCE-ID':(0, 1, None), - 'ATTACH': (0, None, None), - 'ATTENDEE': (0, None, None), - 'CATEGORIES': (0, None, None), - 'COMMENT': (0, None, None), - 'CONTACT': (0, None, None), - 'EXDATE': (0, None, None), - 'EXRULE': (0, None, None), - 'REQUEST-STATUS': (0, None, None), - 'RELATED-TO': (0, None, None), - 'RDATE': (0, None, None), - 'RRULE': (0, None, None) - } -registerBehavior(VJournal) - - -class VFreeBusy(behavior.Behavior): - """Free/busy state behavior. - - >>> vfb = newFromBehavior('VFREEBUSY') - >>> vfb.add('uid').value = 'test' - >>> vfb.add('dtstart').value = datetime.datetime(2006, 2, 16, 1, tzinfo=utc) - >>> vfb.add('dtend').value = vfb.dtstart.value + twoHours - >>> vfb.add('freebusy').value = [(vfb.dtstart.value, twoHours / 2)] - >>> print vfb.serialize() - BEGIN:VFREEBUSY - UID:test - DTSTART:20060216T010000Z - DTEND:20060216T030000Z - FREEBUSY:20060216T010000Z/PT1H - END:VFREEBUSY - - """ - name='VFREEBUSY' - isComponent = True - description='A grouping of component properties that describe either a \ - request for free/busy time, describe a response to a request \ - for free/busy time or describe a published set of busy time.' - sortFirst = ('uid', 'dtstart', 'duration', 'dtend') - knownChildren = {'DTSTART': (0, 1, None),#min, max, behaviorRegistry id - 'CONTACT': (0, 1, None), - 'DTEND': (0, 1, None), - 'DURATION': (0, 1, None), - 'ORGANIZER': (0, 1, None), - 'DTSTAMP': (0, 1, None), - 'UID': (0, 1, None), - 'URL': (0, 1, None), - 'ATTENDEE': (0, None, None), - 'COMMENT': (0, None, None), - 'FREEBUSY': (0, None, None), - 'REQUEST-STATUS': (0, None, None) - } -registerBehavior(VFreeBusy) - - -class VAlarm(behavior.Behavior): - """Alarm behavior.""" - name='VALARM' - isComponent = True - description='Alarms describe when and how to provide alerts about events \ - and to-dos.' - knownChildren = {'ACTION': (1, 1, None),#min, max, behaviorRegistry id - 'TRIGGER': (1, 1, None), - 'DURATION': (0, 1, None), - 'REPEAT': (0, 1, None), - 'DESCRIPTION': (0, 1, None) - } - - def generateImplicitParameters(obj): - """Create default ACTION and TRIGGER if they're not set.""" - try: - obj.action - except AttributeError: - obj.add('action').value = 'AUDIO' - try: - obj.trigger - except AttributeError: - obj.add('trigger').value = datetime.timedelta(0) - generateImplicitParameters = staticmethod(generateImplicitParameters) - - - def validate(cls, obj, raiseException, *args): - """ - #TODO - audioprop = 2*( - - ; 'action' and 'trigger' are both REQUIRED, - ; but MUST NOT occur more than once - - action / trigger / - - ; 'duration' and 'repeat' are both optional, - ; and MUST NOT occur more than once each, - ; but if one occurs, so MUST the other - - duration / repeat / - - ; the following is optional, - ; but MUST NOT occur more than once - - attach / - - dispprop = 3*( - - ; the following are all REQUIRED, - ; but MUST NOT occur more than once - - action / description / trigger / - - ; 'duration' and 'repeat' are both optional, - ; and MUST NOT occur more than once each, - ; but if one occurs, so MUST the other - - duration / repeat / - - emailprop = 5*( - - ; the following are all REQUIRED, - ; but MUST NOT occur more than once - - action / description / trigger / summary - - ; the following is REQUIRED, - ; and MAY occur more than once - - attendee / - - ; 'duration' and 'repeat' are both optional, - ; and MUST NOT occur more than once each, - ; but if one occurs, so MUST the other - - duration / repeat / - - procprop = 3*( - - ; the following are all REQUIRED, - ; but MUST NOT occur more than once - - action / attach / trigger / - - ; 'duration' and 'repeat' are both optional, - ; and MUST NOT occur more than once each, - ; but if one occurs, so MUST the other - - duration / repeat / - - ; 'description' is optional, - ; and MUST NOT occur more than once - - description / - if obj.contents.has_key('DTEND') and obj.contents.has_key('DURATION'): - if raiseException: - m = "VEVENT components cannot contain both DTEND and DURATION\ - components" - raise ValidateError(m) - return False - else: - return super(VEvent, cls).validate(obj, raiseException, *args) - """ - return True - validate = classmethod(validate) - -registerBehavior(VAlarm) - -class Duration(behavior.Behavior): - """Behavior for Duration ContentLines. Transform to datetime.timedelta.""" - name = 'DURATION' - hasNative = True - - def transformToNative(obj): - """Turn obj.value into a datetime.timedelta.""" - if obj.isNative: return obj - obj.isNative = True - obj.value=str(obj.value) - if obj.value == '': - return obj - else: - deltalist=stringToDurations(obj.value) - #When can DURATION have multiple durations? For now: - if len(deltalist) == 1: - obj.value = deltalist[0] - return obj - else: - raise ParseError("DURATION must have a single duration string.") - transformToNative = staticmethod(transformToNative) - - def transformFromNative(obj): - """Replace the datetime.timedelta in obj.value with an RFC2445 string. - """ - if not obj.isNative: return obj - obj.isNative = False - obj.value = timedeltaToString(obj.value) - return obj - transformFromNative = staticmethod(transformFromNative) - -registerBehavior(Duration) - -class Trigger(behavior.Behavior): - """DATE-TIME or DURATION""" - name='TRIGGER' - description='This property specifies when an alarm will trigger.' - hasNative = True - - def transformToNative(obj): - """Turn obj.value into a timedelta or datetime.""" - value = getattr(obj, 'value_param', 'DURATION').upper() - if hasattr(obj, 'value_param'): - del obj.value_param - if obj.value == '': - obj.isNative = True - return obj - elif value == 'DURATION': - return Duration.transformToNative(obj) - elif value == 'DATE-TIME': - #TRIGGERs with DATE-TIME values must be in UTC, we could validate - #that fact, for now we take it on faith. - return DateTimeBehavior.transformToNative(obj) - else: - raise ParseError("VALUE must be DURATION or DATE-TIME") - transformToNative = staticmethod(transformToNative) - - def transformFromNative(obj): - if type(obj.value) == datetime.datetime: - obj.value_param = 'DATE-TIME' - return DateTimeBehavior.transformFromNative(obj) - elif type(obj.value) == datetime.timedelta: - return Duration.transformFromNative(obj) - else: - raise NativeError("Native TRIGGER values must be timedelta or datetime") - transformFromNative = staticmethod(transformFromNative) - -registerBehavior(Trigger) - -class PeriodBehavior(behavior.Behavior): - """A list of (date-time, timedelta) tuples. - - >>> line = ContentLine('test', [], '', isNative=True) - >>> line.behavior = PeriodBehavior - >>> line.value = [(datetime.datetime(2006, 2, 16, 10), twoHours)] - >>> line.transformFromNative().value - '20060216T100000/PT2H' - >>> line.transformToNative().value - [(datetime.datetime(2006, 2, 16, 10, 0), datetime.timedelta(0, 7200))] - >>> line.value.append((datetime.datetime(2006, 5, 16, 10), twoHours)) - >>> print line.serialize().strip() - TEST:20060216T100000/PT2H,20060516T100000/PT2H - """ - hasNative = True - - def transformToNative(obj): - """Convert comma separated periods into tuples.""" - if obj.isNative: - return obj - obj.isNative = True - if obj.value == '': - obj.value = [] - return obj - tzinfo = getTzid(getattr(obj, 'tzid_param', None)) - obj.value = [stringToPeriod(x, tzinfo) for x in obj.value.split(",")] - return obj - transformToNative = staticmethod(transformToNative) - - def transformFromNative(obj, convertToUTC = False): - """Convert the list of tuples in obj.value to strings.""" - if obj.isNative: - obj.isNative = False - transformed = [] - for tup in obj.value: - transformed.append(periodToString(tup, convertToUTC)) - if len(transformed) > 0: - tzid = TimezoneComponent.registerTzinfo(tup[0].tzinfo) - if not convertToUTC and tzid is not None: - obj.tzid_param = tzid - - obj.value = ','.join(transformed) - - return obj - transformFromNative = staticmethod(transformFromNative) - -class FreeBusy(PeriodBehavior): - """Free or busy period of time, must be specified in UTC.""" - name = 'FREEBUSY' - - def transformFromNative(obj): - """Pass convertToUTC = True to parent method.""" - return PeriodBehavior.transformFromNative(obj, True) - transformFromNative = staticmethod(transformFromNative) -registerBehavior(FreeBusy) - -#------------------------ Registration of common classes ----------------------- - -utcDateTimeList = ['LAST-MODIFIED', 'CREATED', 'COMPLETED', 'DTSTAMP'] -list(map(lambda x: registerBehavior(UTCDateTimeBehavior, x), utcDateTimeList)) - -dateTimeOrDateList = ['DTEND', 'DTSTART', 'DUE', 'RECURRENCE-ID'] -list(map(lambda x: registerBehavior(DateOrDateTimeBehavior, x), - dateTimeOrDateList)) - -registerBehavior(MultiDateBehavior, 'RDATE') -registerBehavior(MultiDateBehavior, 'EXDATE') - - -textList = ['CALSCALE', 'METHOD', 'PRODID', 'CLASS', 'COMMENT', 'DESCRIPTION', - 'LOCATION', 'STATUS', 'SUMMARY', 'TRANSP', 'CONTACT', 'RELATED-TO', - 'UID', 'ACTION', 'REQUEST-STATUS', 'TZID'] -list(map(lambda x: registerBehavior(TextBehavior, x), textList)) - -multiTextList = ['CATEGORIES', 'RESOURCES'] -list(map(lambda x: registerBehavior(MultiTextBehavior, x), multiTextList)) - -#------------------------ Serializing helper functions ------------------------- - -def numToDigits(num, places): - """Helper, for converting numbers to textual digits.""" - s = str(num) - if len(s) < places: - return ("0" * (places - len(s))) + s - elif len(s) > places: - return s[len(s)-places: ] - else: - return s - -def timedeltaToString(delta): - """Convert timedelta to an rfc2445 DURATION.""" - if delta.days == 0: sign = 1 - else: sign = delta.days / abs(delta.days) - delta = abs(delta) - days = delta.days - hours = delta.seconds / 3600 - minutes = (delta.seconds % 3600) / 60 - seconds = delta.seconds % 60 - out = '' - if sign == -1: out = '-' - out += 'P' - if days: out += str(days) + 'D' - if hours or minutes or seconds: out += 'T' - elif not days: #Deal with zero duration - out += '0S' - if hours: out += str(hours) + 'H' - if minutes: out += str(minutes) + 'M' - if seconds: out += str(seconds) + 'S' - return out - -def dateToString(date): - year = numToDigits( date.year, 4 ) - month = numToDigits( date.month, 2 ) - day = numToDigits( date.day, 2 ) - return year + month + day - -def dateTimeToString(dateTime, convertToUTC=False): - """Ignore tzinfo unless convertToUTC. Output string.""" - if dateTime.tzinfo and convertToUTC: - dateTime = dateTime.astimezone(utc) - if tzinfo_eq(dateTime.tzinfo, utc): utcString = "Z" - else: utcString = "" - - year = numToDigits( dateTime.year, 4 ) - month = numToDigits( dateTime.month, 2 ) - day = numToDigits( dateTime.day, 2 ) - hour = numToDigits( dateTime.hour, 2 ) - mins = numToDigits( dateTime.minute, 2 ) - secs = numToDigits( dateTime.second, 2 ) - - return year + month + day + "T" + hour + mins + secs + utcString - -def deltaToOffset(delta): - absDelta = abs(delta) - hours = absDelta.seconds / 3600 - hoursString = numToDigits(hours, 2) - minutesString = '00' - if absDelta == delta: - signString = "+" - else: - signString = "-" - return signString + hoursString + minutesString - -def periodToString(period, convertToUTC=False): - txtstart = dateTimeToString(period[0], convertToUTC) - if isinstance(period[1], datetime.timedelta): - txtend = timedeltaToString(period[1]) - else: - txtend = dateTimeToString(period[1], convertToUTC) - return txtstart + "/" + txtend - -#----------------------- Parsing functions ------------------------------------- - -def isDuration(s): - s = string.upper(s) - return (string.find(s, "P") != -1) and (string.find(s, "P") < 2) - -def stringToDate(s, tzinfos=None): - if tzinfos != None: print("Didn't expect a tzinfos here") - year = int( s[0:4] ) - month = int( s[4:6] ) - day = int( s[6:8] ) - return datetime.date(year,month,day) - -def stringToDateTime(s, tzinfo=None): - """Returns datetime.datetime object.""" - try: - year = int( s[0:4] ) - month = int( s[4:6] ) - day = int( s[6:8] ) - hour = int( s[9:11] ) - minute = int( s[11:13] ) - second = int( s[13:15] ) - if len(s) > 15: - if s[15] == 'Z': - tzinfo = utc - except: - raise ParseError("'%s' is not a valid DATE-TIME" % s) - return datetime.datetime(year, month, day, hour, minute, second, 0, tzinfo) - - -escapableCharList = "\\;,Nn" - -def stringToTextValues(s, strict=False): - """Returns list of strings.""" - - def escapableChar (c): - return c in escapableCharList - - def error(msg): - if strict: - raise ParseError(msg) - else: - #logger.error(msg) - print(msg) - - #vars which control state machine - charIterator = enumerate(s) - state = "read normal" - - current = "" - results = [] - - while True: - try: - charIndex, char = next(charIterator) - except: - char = "eof" - - if state == "read normal": - if char == '\\': - state = "read escaped char" - elif char == ',': - state = "read normal" - results.append(current) - current = "" - elif char == "eof": - state = "end" - else: - state = "read normal" - current = current + char - - elif state == "read escaped char": - if escapableChar(char): - state = "read normal" - if char in 'nN': - current = current + '\n' - else: - current = current + char - else: - state = "read normal" - current = current + char #this is an error, but whatever - - elif state == "end": #an end state - if current != "" or len(results) == 0: - results.append(current) - return results - - elif state == "error": #an end state - return results - - else: - state = "error" - error("error: unknown state: '%s' reached in %s" % (state, s)) - -def stringToDurations(s, strict=False): - """Returns list of timedelta objects.""" - def makeTimedelta(sign, week, day, hour, minute, sec): - if sign == "-": sign = -1 - else: sign = 1 - week = int(week) - day = int(day) - hour = int(hour) - minute = int(minute) - sec = int(sec) - return sign * datetime.timedelta(weeks=week, days=day, hours=hour, minutes=minute, seconds=sec) - - def error(msg): - if strict: - raise ParseError(msg) - else: - raise ParseError(msg) - #logger.error(msg) - - #vars which control state machine - charIterator = enumerate(s) - state = "start" - - durations = [] - current = "" - sign = None - week = 0 - day = 0 - hour = 0 - minute = 0 - sec = 0 - - while True: - try: - charIndex, char = next(charIterator) - except: - charIndex += 1 - char = "eof" - - if state == "start": - if char == '+': - state = "start" - sign = char - elif char == '-': - state = "start" - sign = char - elif char.upper() == 'P': - state = "read field" - elif char == "eof": - state = "error" - error("got end-of-line while reading in duration: " + s) - elif char in string.digits: - state = "read field" - current = current + char #update this part when updating "read field" - else: - state = "error" - print("got unexpected character %s reading in duration: %s" % (char, s)) - error("got unexpected character %s reading in duration: %s" % (char, s)) - - elif state == "read field": - if (char in string.digits): - state = "read field" - current = current + char #update part above when updating "read field" - elif char.upper() == 'T': - state = "read field" - elif char.upper() == 'W': - state = "read field" - week = current - current = "" - elif char.upper() == 'D': - state = "read field" - day = current - current = "" - elif char.upper() == 'H': - state = "read field" - hour = current - current = "" - elif char.upper() == 'M': - state = "read field" - minute = current - current = "" - elif char.upper() == 'S': - state = "read field" - sec = current - current = "" - elif char == ",": - state = "start" - durations.append( makeTimedelta(sign, week, day, hour, minute, sec) ) - current = "" - sign = None - week = None - day = None - hour = None - minute = None - sec = None - elif char == "eof": - state = "end" - else: - state = "error" - error("got unexpected character reading in duration: " + s) - - elif state == "end": #an end state - #print "stuff: %s, durations: %s" % ([current, sign, week, day, hour, minute, sec], durations) - - if (sign or week or day or hour or minute or sec): - durations.append( makeTimedelta(sign, week, day, hour, minute, sec) ) - return durations - - elif state == "error": #an end state - error("in error state") - return durations - - else: - state = "error" - error("error: unknown state: '%s' reached in %s" % (state, s)) - -def parseDtstart(contentline): - tzinfo = getTzid(getattr(contentline, 'tzid_param', None)) - valueParam = getattr(contentline, 'value_param', 'DATE-TIME').upper() - if valueParam == "DATE": - return stringToDate(contentline.value) - elif valueParam == "DATE-TIME": - return stringToDateTime(contentline.value, tzinfo) - -def stringToPeriod(s, tzinfo=None): - values = string.split(s, "/") - start = stringToDateTime(values[0], tzinfo) - valEnd = values[1] - if isDuration(valEnd): #period-start = date-time "/" dur-value - delta = stringToDurations(valEnd)[0] - return (start, delta) - else: - return (start, stringToDateTime(valEnd, tzinfo) - start) - - -def getTransition(transitionTo, year, tzinfo): - """Return the datetime of the transition to/from DST, or None.""" - - def firstTransition(iterDates, test): - """ - Return the last date not matching test, or None if all tests matched. - """ - success = None - for dt in iterDates: - if not test(dt): - success = dt - else: - if success is not None: - return success - return success # may be None - - def generateDates(year, month=None, day=None): - """Iterate over possible dates with unspecified values.""" - months = list(range(1, 13)) - days = list(range(1, 32)) - hours = list(range(0, 24)) - if month is None: - for month in months: - yield datetime.datetime(year, month, 1) - elif day is None: - for day in days: - try: - yield datetime.datetime(year, month, day) - except ValueError: - pass - else: - for hour in hours: - yield datetime.datetime(year, month, day, hour) - - assert transitionTo in ('daylight', 'standard') - if transitionTo == 'daylight': - def test(dt): return tzinfo.dst(dt) != zeroDelta - elif transitionTo == 'standard': - def test(dt): return tzinfo.dst(dt) == zeroDelta - newyear = datetime.datetime(year, 1, 1) - monthDt = firstTransition(generateDates(year), test) - if monthDt is None: - return newyear - elif monthDt.month == 12: - return None - else: - # there was a good transition somewhere in a non-December month - month = monthDt.month - day = firstTransition(generateDates(year, month), test).day - uncorrected = firstTransition(generateDates(year, month, day), test) - if transitionTo == 'standard': - # assuming tzinfo.dst returns a new offset for the first - # possible hour, we need to add one hour for the offset change - # and another hour because firstTransition returns the hour - # before the transition - return uncorrected + datetime.timedelta(hours=2) - else: - return uncorrected + datetime.timedelta(hours=1) - -def tzinfo_eq(tzinfo1, tzinfo2, startYear = 2000, endYear=2020): - """Compare offsets and DST transitions from startYear to endYear.""" - if tzinfo1 == tzinfo2: - return True - elif tzinfo1 is None or tzinfo2 is None: - return False - - def dt_test(dt): - if dt is None: - return True - return tzinfo1.utcoffset(dt) == tzinfo2.utcoffset(dt) - - if not dt_test(datetime.datetime(startYear, 1, 1)): - return False - for year in range(startYear, endYear): - for transitionTo in 'daylight', 'standard': - t1=getTransition(transitionTo, year, tzinfo1) - t2=getTransition(transitionTo, year, tzinfo2) - if t1 != t2 or not dt_test(t1): - return False - return True - - -#------------------- Testing and running functions ----------------------------- -if __name__ == '__main__': - from . import tests - tests._test() diff --git a/src/libs/python/bongo/external/vobject/tests.py b/src/libs/python/bongo/external/vobject/tests.py deleted file mode 100644 index 84920df..0000000 --- a/src/libs/python/bongo/external/vobject/tests.py +++ /dev/null @@ -1,601 +0,0 @@ -"""Long or boring tests for vobjects.""" - -import base, behavior, io, icalendar, vcard, re, dateutil.tz, datetime - -# setuptools is required to run the unit tests -from pkg_resources import resource_stream - -#------------------- Testing and running functions ----------------------------- -def _test(): - import doctest, base, tests, icalendar, __init__, re - flags = doctest.NORMALIZE_WHITESPACE | doctest.REPORT_UDIFF \ - | doctest.REPORT_ONLY_FIRST_FAILURE - for mod in base, tests, icalendar, __init__: - doctest.testmod(mod, verbose=0, optionflags=flags) - try: - doctest.testfile('../../README.txt', optionflags=flags) - except IOError: #allow this test to fail if we can't find README.txt - pass - - -if __name__ == '__main__': - _test() - - -testSilly=""" -sillyname:name -profile:sillyprofile -stuff:folded - line -""" + "morestuff;asinine:this line is not folded, \ -but in practice probably ought to be, as it is exceptionally long, \ -and moreover demonstratively stupid" - -icaltest=r"""BEGIN:VCALENDAR -CALSCALE:GREGORIAN -X-WR-TIMEZONE;VALUE=TEXT:US/Pacific -METHOD:PUBLISH -PRODID:-//Apple Computer\, Inc//iCal 1.0//EN -X-WR-CALNAME;VALUE=TEXT:Example -VERSION:2.0 -BEGIN:VEVENT -SEQUENCE:5 -DTSTART;TZID=US/Pacific:20021028T140000 -RRULE:FREQ=Weekly;COUNT=10 -DTSTAMP:20021028T011706Z -SUMMARY:Coffee with Jason -UID:EC9439B1-FF65-11D6-9973-003065F99D04 -DTEND;TZID=US/Pacific:20021028T150000 -BEGIN:VALARM -TRIGGER;VALUE=DURATION:-P1D -ACTION:DISPLAY -DESCRIPTION:Event reminder\, with comma\nand line feed -END:VALARM -END:VEVENT -BEGIN:VTIMEZONE -X-LIC-LOCATION:Random location -TZID:US/Pacific -LAST-MODIFIED:19870101T000000Z -BEGIN:STANDARD -DTSTART:19671029T020000 -RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 -TZOFFSETFROM:-0700 -TZOFFSETTO:-0800 -TZNAME:PST -END:STANDARD -BEGIN:DAYLIGHT -DTSTART:19870405T020000 -RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 -TZOFFSETFROM:-0800 -TZOFFSETTO:-0700 -TZNAME:PDT -END:DAYLIGHT -END:VTIMEZONE -END:VCALENDAR""" - -vcardtest = """BEGIN:VCARD -VERSION:3.0 -FN:Daffy Duck Knudson (with Bugs Bunny and Mr. Pluto) -N:Knudson;Daffy Duck (with Bugs Bunny and Mr. Pluto) -NICKNAME:gnat and gnu and pluto -BDAY;value=date:02-10;11-05;01-01 -TEL;type=HOME:+01-(0)2-765.43.21 -TEL;type=CELL:+01-(0)5-555.55.55 -ACCOUNT;type=HOME:010-1234567-05 -ADR;type=HOME:;;Haight Street 512;Novosibirsk;;80214;Gnuland -TEL;type=HOME:+01-(0)2-876.54.32 -ORG:University of Novosibirsk, Department of Octopus - Parthenogenesis -END:VCARD""" - -vcardWithGroups = r"""home.begin:vcard -version:3.0 -source:ldap://cn=Meister%20Berger,o=Universitaet%20Goerlitz,c=DE -name:Meister Berger -fn:Meister Berger -n:Berger;Meister -bday;value=date:1963-09-21 -o:Universit=E6t G=F6rlitz -title:Mayor -title;language=de;value=text:Burgermeister -note:The Mayor of the great city of - Goerlitz in the great country of Germany. -email;internet:mb@goerlitz.de -home.tel;type=fax,voice;type=msg:+49 3581 123456 -home.label:Hufenshlagel 1234\n - 02828 Goerlitz\n - Deutschland -END:VCARD""" - -lowercaseComponentNames = r"""begin:vcard -fn:Anders Bobo -n:Bobo;Anders -org:Bobo A/S;Vice President, Technical Support -adr:Rockfeller Center;;Mekastreet;Bobocity;;2100;Myworld -email;internet:bobo@example.com -tel;work:+123455 -tel;fax:+123456 -tel;cell:+123457 -x-mozilla-html:FALSE -url:http://www.example.com -version:2.1 -end:vcard""" - -badstream = r"""BEGIN:VCALENDAR -CALSCALE:GREGORIAN -X-WR-TIMEZONE;VALUE=TEXT:US/Pacific -METHOD:PUBLISH -PRODID:-//Apple Computer\, Inc//iCal 1.0//EN -X-WR-CALNAME;VALUE=TEXT:Example -VERSION:2.0 -BEGIN:VEVENT -DTSTART:20021028T140000Z -BEGIN:VALARM -TRIGGER:20021028T120000Z -ACTION:DISPLAY -DESCRIPTION:This trigger is a date-time without a VALUE=DATE-TIME parameter -END:VALARM -END:VEVENT -END:VCALENDAR""" - -timezones = r""" - -BEGIN:VTIMEZONE -TZID:US/Pacific -BEGIN:STANDARD -DTSTART:19671029T020000 -RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 -TZOFFSETFROM:-0700 -TZOFFSETTO:-0800 -TZNAME:PST -END:STANDARD -BEGIN:DAYLIGHT -DTSTART:19870405T020000 -RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 -TZOFFSETFROM:-0800 -TZOFFSETTO:-0700 -TZNAME:PDT -END:DAYLIGHT -END:VTIMEZONE - -BEGIN:VTIMEZONE -TZID:US/Eastern -BEGIN:STANDARD -DTSTART:19671029T020000 -RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 -TZOFFSETFROM:-0400 -TZOFFSETTO:-0500 -TZNAME:EST -END:STANDARD -BEGIN:DAYLIGHT -DTSTART:19870405T020000 -RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 -TZOFFSETFROM:-0500 -TZOFFSETTO:-0400 -TZNAME:EDT -END:DAYLIGHT -END:VTIMEZONE - -BEGIN:VTIMEZONE -TZID:Santiago -BEGIN:STANDARD -DTSTART:19700314T000000 -TZOFFSETFROM:-0300 -TZOFFSETTO:-0400 -RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SA -TZNAME:Pacific SA Standard Time -END:STANDARD -BEGIN:DAYLIGHT -DTSTART:19701010T000000 -TZOFFSETFROM:-0400 -TZOFFSETTO:-0300 -RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=2SA -TZNAME:Pacific SA Daylight Time -END:DAYLIGHT -END:VTIMEZONE - -BEGIN:VTIMEZONE -TZID:W. Europe -BEGIN:STANDARD -DTSTART:19701025T030000 -TZOFFSETFROM:+0200 -TZOFFSETTO:+0100 -RRULE:FREQ=YEARLY;BYMONTH=10;BYDAY=-1SU -TZNAME:W. Europe Standard Time -END:STANDARD -BEGIN:DAYLIGHT -DTSTART:19700329T020000 -TZOFFSETFROM:+0100 -TZOFFSETTO:+0200 -RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=-1SU -TZNAME:W. Europe Daylight Time -END:DAYLIGHT -END:VTIMEZONE - -BEGIN:VTIMEZONE -TZID:US/Fictitious-Eastern -LAST-MODIFIED:19870101T000000Z -BEGIN:STANDARD -DTSTART:19671029T020000 -RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 -TZOFFSETFROM:-0400 -TZOFFSETTO:-0500 -TZNAME:EST -END:STANDARD -BEGIN:DAYLIGHT -DTSTART:19870405T020000 -RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4;UNTIL=20050403T070000Z -TZOFFSETFROM:-0500 -TZOFFSETTO:-0400 -TZNAME:EDT -END:DAYLIGHT -END:VTIMEZONE - -BEGIN:VTIMEZONE -TZID:America/Montreal -LAST-MODIFIED:20051013T233643Z -BEGIN:DAYLIGHT -DTSTART:20050403T070000 -TZOFFSETTO:-0400 -TZOFFSETFROM:+0000 -TZNAME:EDT -END:DAYLIGHT -BEGIN:STANDARD -DTSTART:20051030T020000 -TZOFFSETTO:-0500 -TZOFFSETFROM:-0400 -TZNAME:EST -END:STANDARD -END:VTIMEZONE - -""" - -__test__ = { "Test readOne" : - r""" - >>> silly = base.readOne(testSilly) - >>> silly - , , ]> - >>> silly.stuff - - >>> original = silly.serialize() - >>> f3 = StringIO.StringIO(original) - >>> silly2 = base.readOne(f3) - >>> silly2.serialize()==original - True - >>> s3 = StringIO.StringIO('cn:Babs Jensen\r\ncn:Barbara J Jensen\r\nsn:Jensen\r\nemail:babs@umich.edu\r\nphone:+1 313 747-4454\r\nx-id:1234567890\r\n') - >>> ex1 = base.readOne(s3) - >>> ex1 - <*unnamed*| [, , , , , ]> - >>> ex1.serialize() - u'CN:Babs Jensen\r\nCN:Barbara J Jensen\r\nEMAIL:babs@umich.edu\r\nPHONE:+1 313 747-4454\r\nSN:Jensen\r\nX-ID:1234567890\r\n' - """, - - "Import icaltest" : - r""" - >>> c = base.readOne(icaltest, validate=True) - >>> c.vevent.valarm.trigger - - >>> c.vevent.dtstart.value - datetime.datetime(2002, 10, 28, 14, 0, tzinfo=) - >>> c.vevent.dtend.value - datetime.datetime(2002, 10, 28, 15, 0, tzinfo=) - >>> c.vevent.dtstamp.value - datetime.datetime(2002, 10, 28, 1, 17, 6, tzinfo=tzutc()) - >>> c.vevent.valarm.description.value - u'Event reminder, with comma\nand line feed' - >>> c.vevent.valarm.description.serialize() - u'DESCRIPTION:Event reminder\\, with comma\\nand line feed\r\n' - >>> vevent = c.vevent.transformFromNative() - >>> vevent.rrule - - """, - - "Parsing tests" : - """ - >>> parseRDate = icalendar.MultiDateBehavior.transformToNative - >>> icalendar.stringToTextValues('') - [''] - >>> icalendar.stringToTextValues('abcd,efgh') - ['abcd', 'efgh'] - >>> icalendar.stringToPeriod("19970101T180000Z/19970102T070000Z") - (datetime.datetime(1997, 1, 1, 18, 0, tzinfo=tzutc()), datetime.timedelta(0, 46800)) - >>> parseRDate(base.textLineToContentLine("RDATE;VALUE=DATE:19970304,19970504,19970704,19970904")) - - >>> parseRDate(base.textLineToContentLine("RDATE;VALUE=PERIOD:19960403T020000Z/19960403T040000Z,19960404T010000Z/PT3H")) - - """, - - "read failure" : - """ - >>> f = StringIO.StringIO(badstream) - >>> vevent = base.readOne(f) - Traceback (most recent call last): - ... - ParseError: At line 11: got unexpected character reading in duration: 20021028T120000Z - """, - - "unicode test" : - r""" - >>> f = resource_stream(__name__, 'utf8_test.ics') - >>> vevent = base.readOne(f).vevent - >>> vevent.summary.value - u'The title \u3053\u3093\u306b\u3061\u306f\u30ad\u30c6\u30a3' - >>> summary = vevent.summary.value - """, - - "regular expression test" : - """ - >>> re.findall(base.patterns['name'], '12foo-bar:yay') - ['12foo-bar', 'yay'] - >>> re.findall(base.patterns['safe_char'], 'a;b"*,cd') - ['a', 'b', '*', 'c', 'd'] - >>> re.findall(base.patterns['qsafe_char'], 'a;b"*,cd') - ['a', ';', 'b', '*', ',', 'c', 'd'] - >>> re.findall(base.patterns['param_value'], '"quoted";not-quoted;start"after-illegal-quote', re.VERBOSE) - ['"quoted"', '', 'not-quoted', '', 'start', '', 'after-illegal-quote', ''] - >>> match = base.line_re.match('TEST;ALTREP="http://www.wiz.org":value:;"') - >>> match.group('value') - 'value:;"' - >>> match.group('name') - 'TEST' - >>> match.group('params') - ';ALTREP="http://www.wiz.org"' - """, - - "VTIMEZONE creation test:" : - - """ - >>> f = StringIO.StringIO(timezones) - >>> tzs = dateutil.tz.tzical(f) - >>> tzs.get("US/Pacific") - - >>> icalendar.TimezoneComponent(_) - > - >>> pacific = _ - >>> print pacific.serialize() - BEGIN:VTIMEZONE - TZID:US/Pacific - BEGIN:STANDARD - DTSTART:20001029T020000 - RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 - TZNAME:PST - TZOFFSETFROM:-0700 - TZOFFSETTO:-0800 - END:STANDARD - BEGIN:DAYLIGHT - DTSTART:20000402T020000 - RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 - TZNAME:PDT - TZOFFSETFROM:-0800 - TZOFFSETTO:-0700 - END:DAYLIGHT - END:VTIMEZONE - >>> (_) - > - >>> santiago = icalendar.TimezoneComponent(tzs.get('Santiago')) - >>> ser = santiago.serialize() - >>> print ser - BEGIN:VTIMEZONE - TZID:Santiago - BEGIN:STANDARD - DTSTART:20000311T000000 - RRULE:FREQ=YEARLY;BYDAY=2SA;BYMONTH=3 - TZNAME:Pacific SA Standard Time - TZOFFSETFROM:-0300 - TZOFFSETTO:-0400 - END:STANDARD - BEGIN:DAYLIGHT - DTSTART:20001014T000000 - RRULE:FREQ=YEARLY;BYDAY=2SA;BYMONTH=10 - TZNAME:Pacific SA Daylight Time - TZOFFSETFROM:-0400 - TZOFFSETTO:-0300 - END:DAYLIGHT - END:VTIMEZONE - >>> roundtrip = dateutil.tz.tzical(StringIO.StringIO(str(ser))).get() - >>> for year in range(2001, 2010): - ... for month in (2, 9): - ... dt = datetime.datetime(year, month, 15, tzinfo = roundtrip) - ... if dt.replace(tzinfo=tzs.get('Santiago')) != dt: - ... print "Failed for:", dt - >>> fict = icalendar.TimezoneComponent(tzs.get('US/Fictitious-Eastern')) - >>> print fict.serialize() - BEGIN:VTIMEZONE - TZID:US/Fictitious-Eastern - BEGIN:STANDARD - DTSTART:20001029T020000 - RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 - TZNAME:EST - TZOFFSETFROM:-0400 - TZOFFSETTO:-0500 - END:STANDARD - BEGIN:DAYLIGHT - DTSTART:20000402T020000 - RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4;UNTIL=20050403T070000Z - TZNAME:EDT - TZOFFSETFROM:-0500 - TZOFFSETTO:-0400 - END:DAYLIGHT - END:VTIMEZONE - """, - - "Serializing with timezones test" : - - """ - >>> from dateutil.rrule import rrule, rruleset, WEEKLY - >>> pacific = dateutil.tz.tzical(StringIO.StringIO(timezones)).get('US/Pacific') - >>> cal = base.Component('VCALENDAR') - >>> cal.setBehavior(icalendar.VCalendar2_0) - >>> ev = cal.add('vevent') - >>> ev.add('dtstart').value = datetime.datetime(2005, 10, 12, 9, tzinfo = pacific) - >>> set = rruleset() - >>> set.rrule(rrule(WEEKLY, interval=2, byweekday=[2,4], until=datetime.datetime(2005, 12, 15, 9))) - >>> set.exdate(datetime.datetime(2005, 10, 14, 9, tzinfo = pacific)) - >>> ev.rruleset = set - >>> ev.add('uid').value = "uid could be generated but doctest complains" - >>> ev.add('duration').value = datetime.timedelta(hours=1) - >>> print cal.serialize() - BEGIN:VCALENDAR - VERSION:2.0 - PRODID:-//PYVOBJECT//NONSGML Version 1//EN - BEGIN:VTIMEZONE - TZID:US/Pacific - BEGIN:STANDARD - DTSTART:20001029T020000 - RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 - TZNAME:PST - TZOFFSETFROM:-0700 - TZOFFSETTO:-0800 - END:STANDARD - BEGIN:DAYLIGHT - DTSTART:20000402T020000 - RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 - TZNAME:PDT - TZOFFSETFROM:-0800 - TZOFFSETTO:-0700 - END:DAYLIGHT - END:VTIMEZONE - BEGIN:VEVENT - UID:uid could be generated but doctest complains - DTSTART;TZID=US/Pacific:20051012T090000 - DURATION:PT1H - EXDATE;TZID=US/Pacific:20051014T090000 - RRULE:FREQ=WEEKLY;BYDAY=WE,FR;INTERVAL=2;UNTIL=20051215T090000 - END:VEVENT - END:VCALENDAR - >>> apple = dateutil.tz.tzical(StringIO.StringIO(timezones)).get('America/Montreal') - >>> ev.dtstart.value = datetime.datetime(2005, 10, 12, 9, tzinfo = apple) - >>> print cal.serialize() - BEGIN:VCALENDAR - VERSION:2.0 - PRODID:-//PYVOBJECT//NONSGML Version 1//EN - BEGIN:VTIMEZONE - TZID:US/Pacific - BEGIN:STANDARD - DTSTART:20001029T020000 - RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10 - TZNAME:PST - TZOFFSETFROM:-0700 - TZOFFSETTO:-0800 - END:STANDARD - BEGIN:DAYLIGHT - DTSTART:20000402T020000 - RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4 - TZNAME:PDT - TZOFFSETFROM:-0800 - TZOFFSETTO:-0700 - END:DAYLIGHT - END:VTIMEZONE - BEGIN:VTIMEZONE - TZID:America/Montreal - BEGIN:STANDARD - DTSTART:20000101T000000 - RRULE:FREQ=YEARLY;BYMONTH=1;UNTIL=20040101T050000Z - TZNAME:EST - TZOFFSETFROM:-0500 - TZOFFSETTO:-0500 - END:STANDARD - BEGIN:STANDARD - DTSTART:20051030T020000 - RRULE:FREQ=YEARLY;BYDAY=5SU;BYMONTH=10 - TZNAME:EST - TZOFFSETFROM:-0400 - TZOFFSETTO:-0500 - END:STANDARD - BEGIN:DAYLIGHT - DTSTART:20050403T070000 - RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4;UNTIL=20050403T120000Z - TZNAME:EDT - TZOFFSETFROM:-0500 - TZOFFSETTO:-0400 - END:DAYLIGHT - END:VTIMEZONE - BEGIN:VEVENT - UID:uid could be generated but doctest complains - DTSTART;TZID=America/Montreal:20051012T090000 - DURATION:PT1H - EXDATE;TZID=US/Pacific:20051014T090000 - RRULE:FREQ=WEEKLY;BYDAY=WE,FR;INTERVAL=2;UNTIL=20051215T090000 - END:VEVENT - END:VCALENDAR - """, - - "Generate UIDs automatically test:" : - - """ - >>> cal = base.newFromBehavior('vcalendar') - >>> cal.add('vevent').add('dtstart').value = datetime.datetime(2006,2,2,10) - >>> ser = cal.serialize() - >>> len(cal.vevent.uid_list) - 1 - """, - - "VCARD 3.0 parse test:" : - - r""" - >>> card = base.readOne(vcardtest) - >>> card.adr.value - - >>> print card.adr.value - Haight Street 512 - Novosibirsk, 80214 - Gnuland - >>> card.org.value - u'University of Novosibirsk, Department of Octopus Parthenogenesis' - >>> print card.serialize() - BEGIN:VCARD - VERSION:3.0 - ACCOUNT;TYPE=HOME:010-1234567-05 - ADR;TYPE=HOME:;;Haight Street 512;Novosibirsk;;80214;Gnuland - BDAY;VALUE=date:02-10;11-05;01-01 - FN:Daffy Duck Knudson (with Bugs Bunny and Mr. Pluto) - N:Knudson;Daffy Duck (with Bugs Bunny and Mr. Pluto);;; - NICKNAME:gnat and gnu and pluto - ORG:University of Novosibirsk, Department of Octopus Parthenogenesis - TEL;TYPE=HOME:+01-(0)2-765.43.21 - TEL;TYPE=CELL:+01-(0)5-555.55.55 - TEL;TYPE=HOME:+01-(0)2-876.54.32 - END:VCARD - """, - - "Multi-text serialization test:" : - - """ - >>> category = base.newFromBehavior('categories') - >>> category.value = ['Random category'] - >>> print category.serialize().strip() - CATEGORIES:Random category - >>> category.value.append('Other category') - >>> print category.serialize().strip() - CATEGORIES:Random category,Other category - """, - - "vCard groups test:" : - - """ - >>> card = base.readOne(vcardWithGroups) - >>> card.group - u'home' - >>> card.tel.group - u'home' - >>> card.group = card.tel.group = 'new' - >>> card.tel.serialize().strip() - u'new.TEL;TYPE=fax,voice,msg:+49 3581 123456' - >>> card.serialize().splitlines()[0] - u'new.BEGIN:VCARD' - >>> dtstart = base.newFromBehavior('dtstart') - >>> dtstart.group = "badgroup" - >>> dtstart.serialize() - Traceback (most recent call last): - ... - VObjectError: " has a group, but this object doesn't support groups" - """, - - "Lowercase components test:" : - - """ - >>> card = base.readOne(lowercaseComponentNames) - >>> card.version - - """ - } \ No newline at end of file diff --git a/src/libs/python/bongo/external/vobject/vcard.py b/src/libs/python/bongo/external/vobject/vcard.py deleted file mode 100644 index 6961667..0000000 --- a/src/libs/python/bongo/external/vobject/vcard.py +++ /dev/null @@ -1,179 +0,0 @@ -"""Definitions and behavior for vCard 3.0""" - -from . import behavior -import itertools - -from .base import VObjectError, NativeError, ValidateError, ParseError, \ - VBase, Component, ContentLine, logger, defaultSerialize, \ - registerBehavior, backslashEscape - -from .icalendar import TextBehavior - -#------------------------ vCard structs ---------------------------------------- - -class Name(object): - def __init__(self, family = '', given = '', additional = '', prefix = '', - suffix = ''): - """Each name attribute can be a string or a list of strings.""" - self.family = family - self.given = given - self.additional = additional - self.prefix = prefix - self.suffix = suffix - - def toString(val): - """Turn a string or array value into a string.""" - if type(val) in (list, tuple): - return ' '.join(val) - return val - toString = staticmethod(toString) - - def __str__(self): - eng_order = ('prefix', 'given', 'additional', 'family', 'suffix') - return ' '.join([self.toString(getattr(self, val)) for val in eng_order]) - - def __repr__(self): - return "" % self.__str__() - -class Address(object): - def __init__(self, street = '', city = '', region = '', code = '', - country = '', box = '', extended = ''): - """Each name attribute can be a string or a list of strings.""" - self.box = box - self.extended = extended - self.street = street - self.city = city - self.region = region - self.code = code - self.country = country - - def toString(val, join_char='\n'): - """Turn a string or array value into a string.""" - if type(val) in (list, tuple): - return join_char.join(val) - return val - toString = staticmethod(toString) - - lines = ('box', 'extended', 'street') - one_line = ('city', 'region', 'code') - - def __str__(self): - lines = '\n'.join([self.toString(getattr(self, val)) for val in self.lines if getattr(self, val)]) - one_line = tuple([self.toString(getattr(self, val), ' ') for val in self.one_line]) - lines += "\n%s, %s %s" % one_line - if self.country: - lines += '\n' + self.toString(self.country) - return lines - - def __repr__(self): - return "" % self.__str__().replace('\n', '\\n') - -#------------------------ Registered Behavior subclasses ----------------------- -class VCardBehavior(behavior.Behavior): - allowGroup = True - -class VCard3_0(VCardBehavior): - """vCard 3.0 behavior.""" - name = 'VCARD' - description = 'vCard 3.0, defined in rfc2426' - versionString = '3.0' - isComponent = True - sortFirst = ('version', 'prodid', 'uid') - knownChildren = {'N': (1, 1, None),#min, max, behaviorRegistry id - 'FN': (1, 1, None), - 'VERSION': (1, 1, None),#required, auto-generated - 'PRODID': (0, 1, None), - 'LABEL': (0, None, None), - 'UID': (0, None, None), - 'ADR': (0, None, None) - } - - def generateImplicitParameters(cls, obj): - """Create PRODID, VERSION, and VTIMEZONEs if needed. - - VTIMEZONEs will need to exist whenever TZID parameters exist or when - datetimes with tzinfo exist. - - """ - if not hasattr(obj, 'version'): - obj.add(ContentLine('VERSION', [], cls.versionString)) - generateImplicitParameters = classmethod(generateImplicitParameters) -registerBehavior(VCard3_0, default=True) - -class VCardTextBehavior(TextBehavior): - allowGroup = True - base64string = 'B' - -class FN(VCardTextBehavior): - name = "FN" - description = 'Formatted name' -registerBehavior(FN) - -class Label(VCardTextBehavior): - name = "Label" - description = 'Formatted address' -registerBehavior(FN) - -def toListOrString(string): - if string.find(',') >= 0: - return string.split(',') - return string - -def splitFields(string): - """Return a list of strings or lists from a Name or Address.""" - return [toListOrString(i) for i in string.split(';')] - -def toList(stringOrList): - if isinstance(stringOrList, str): - return [stringOrList] - return stringOrList - -def serializeFields(obj, order): - """Turn an object's fields into a ';' and ',' seperated string.""" - return ';'.join([','.join(toList(getattr(obj, val))) for val in order]) - -NAME_ORDER = ('family', 'given', 'additional', 'prefix', 'suffix') - -class NameBehavior(VCardBehavior): - """A structured name.""" - hasNative = True - - def transformToNative(obj): - """Turn obj.value into a Name.""" - if obj.isNative: return obj - obj.isNative = True - obj.value = Name(**dict(list(zip(NAME_ORDER, splitFields(obj.value))))) - return obj - transformToNative = staticmethod(transformToNative) - - def transformFromNative(obj): - """Replace the Name in obj.value with a string.""" - obj.isNative = False - obj.value = serializeFields(obj.value, NAME_ORDER) - return obj - transformFromNative = staticmethod(transformFromNative) -registerBehavior(NameBehavior, 'N') - -ADDRESS_ORDER = ('box', 'extended', 'street', 'city', 'region', 'code', - 'country') - -class AddressBehavior(VCardBehavior): - """A structured address.""" - hasNative = True - - def transformToNative(obj): - """Turn obj.value into an Address.""" - if obj.isNative: return obj - obj.isNative = True - obj.value = Address(**dict(list(zip(ADDRESS_ORDER, splitFields(obj.value))))) - return obj - transformToNative = staticmethod(transformToNative) - - def transformFromNative(obj): - """Replace the Address in obj.value with a string.""" - obj.isNative = False - obj.value = serializeFields(obj.value, ADDRESS_ORDER) - return obj - transformFromNative = staticmethod(transformFromNative) -registerBehavior(AddressBehavior, 'ADR') - diff --git a/src/libs/python/bongo/external/vobject/win32tz.py b/src/libs/python/bongo/external/vobject/win32tz.py deleted file mode 100644 index 4f76aaf..0000000 --- a/src/libs/python/bongo/external/vobject/win32tz.py +++ /dev/null @@ -1,156 +0,0 @@ -import winreg -import struct -import datetime - -handle=winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) -tzparent=winreg.OpenKey(handle, - "SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Time Zones") -parentsize=winreg.QueryInfoKey(tzparent)[0] - -localkey=winreg.OpenKey(handle, - "SYSTEM\\CurrentControlSet\\Control\\TimeZoneInformation") -WEEKS=datetime.timedelta(7) - -def list_timezones(): - """Return a list of all time zones known to the system.""" - l=[] - for i in range(parentsize): - l.append(winreg.EnumKey(tzparent, i)) - return l - -class win32tz(datetime.tzinfo): - """tzinfo class based on win32's timezones available in the registry. - - >>> local = win32tz('Central Standard Time') - >>> oct1 = datetime.datetime(month=10, year=2004, day=1, tzinfo=local) - >>> dec1 = datetime.datetime(month=12, year=2004, day=1, tzinfo=local) - >>> oct1.dst() - datetime.timedelta(0, 3600) - >>> dec1.dst() - datetime.timedelta(0) - >>> braz = win32tz('E. South America Standard Time') - >>> braz.dst(oct1) - datetime.timedelta(0) - >>> braz.dst(dec1) - datetime.timedelta(0, 3600) - - """ - def __init__(self, name): - self.data=win32tz_data(name) - - def utcoffset(self, dt): - if self._isdst(dt): - return datetime.timedelta(minutes=self.data.dstoffset) - else: - return datetime.timedelta(minutes=self.data.stdoffset) - - def dst(self, dt): - if self._isdst(dt): - minutes = self.data.dstoffset - self.data.stdoffset - return datetime.timedelta(minutes=minutes) - else: - return datetime.timedelta(0) - - def tzname(self, dt): - if self._isdst(dt): return self.data.dstname - else: return self.data.stdname - - def _isdst(self, dt): - dat=self.data - dston = pickNthWeekday(dt.year, dat.dstmonth, dat.dstdayofweek, - dat.dsthour, dat.dstminute, dat.dstweeknumber) - dstoff = pickNthWeekday(dt.year, dat.stdmonth, dat.stddayofweek, - dat.stdhour, dat.stdminute, dat.stdweeknumber) - if dston < dstoff: - if dston <= dt.replace(tzinfo=None) < dstoff: return True - else: return False - else: - if dstoff <= dt.replace(tzinfo=None) < dston: return False - else: return True - - def __repr__(self): - return "" % self.data.display - -def pickNthWeekday(year, month, dayofweek, hour, minute, whichweek): - """dayofweek == 0 means Sunday, whichweek > 4 means last instance""" - first = datetime.datetime(year=year, month=month, hour=hour, minute=minute, - day=1) - weekdayone = first.replace(day=((dayofweek - first.isoweekday()) % 7 + 1)) - for n in range(whichweek - 1, -1, -1): - dt=weekdayone + n * WEEKS - if dt.month == month: return dt - - -class win32tz_data(object): - """Read a registry key for a timezone, expose its contents.""" - - def __init__(self, path): - """Load path, or if path is empty, load local time.""" - if path: - keydict=valuesToDict(winreg.OpenKey(tzparent, path)) - self.display = keydict['Display'] - self.dstname = keydict['Dlt'] - self.stdname = keydict['Std'] - - #see http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm - tup = struct.unpack('=3l16h', keydict['TZI']) - self.stdoffset = -tup[0]-tup[1] #Bias + StandardBias * -1 - self.dstoffset = self.stdoffset - tup[2] # + DaylightBias * -1 - - offset=3 - self.stdmonth = tup[1 + offset] - self.stddayofweek = tup[2 + offset] #Sunday=0 - self.stdweeknumber = tup[3 + offset] #Last = 5 - self.stdhour = tup[4 + offset] - self.stdminute = tup[5 + offset] - - offset=11 - self.dstmonth = tup[1 + offset] - self.dstdayofweek = tup[2 + offset] #Sunday=0 - self.dstweeknumber = tup[3 + offset] #Last = 5 - self.dsthour = tup[4 + offset] - self.dstminute = tup[5 + offset] - - else: - keydict=valuesToDict(localkey) - - self.stdname = keydict['StandardName'] - self.dstname = keydict['DaylightName'] - - sourcekey=winreg.OpenKey(tzparent, self.stdname) - self.display = valuesToDict(sourcekey)['Display'] - - self.stdoffset = -keydict['Bias']-keydict['StandardBias'] - self.dstoffset = self.stdoffset - keydict['DaylightBias'] - - #see http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm - tup = struct.unpack('=8h', keydict['StandardStart']) - - offset=0 - self.stdmonth = tup[1 + offset] - self.stddayofweek = tup[2 + offset] #Sunday=0 - self.stdweeknumber = tup[3 + offset] #Last = 5 - self.stdhour = tup[4 + offset] - self.stdminute = tup[5 + offset] - - tup = struct.unpack('=8h', keydict['DaylightStart']) - self.dstmonth = tup[1 + offset] - self.dstdayofweek = tup[2 + offset] #Sunday=0 - self.dstweeknumber = tup[3 + offset] #Last = 5 - self.dsthour = tup[4 + offset] - self.dstminute = tup[5 + offset] - -def valuesToDict(key): - """Convert a registry key's values to a dictionary.""" - dict={} - size=winreg.QueryInfoKey(key)[1] - for i in range(size): - dict[winreg.EnumValue(key, i)[0]]=winreg.EnumValue(key, i)[1] - return dict - -def _test(): - import win32tz, doctest - doctest.testmod(win32tz, verbose=0) - -if __name__ == '__main__': - _test() \ No newline at end of file diff --git a/src/libs/python/bongo/nmap/CommandStream.py b/src/libs/python/bongo/nmap/CommandStream.py index 3be64a6..860a179 100755 --- a/src/libs/python/bongo/nmap/CommandStream.py +++ b/src/libs/python/bongo/nmap/CommandStream.py @@ -191,7 +191,7 @@ class ResponseIterator: def __next__(self): r = self.stream.GetResponse() if r.code == 1000: - if re.match("\d+", r.message): + if re.match(r"\d+", r.message): self.total = int(r.message) self.done = True elif r.code > 3000: diff --git a/src/libs/python/bongo/store/CommandStream.py b/src/libs/python/bongo/store/CommandStream.py index 12e6676..fcedf92 100644 --- a/src/libs/python/bongo/store/CommandStream.py +++ b/src/libs/python/bongo/store/CommandStream.py @@ -91,7 +91,9 @@ class CommandStream: log = logging.getLogger("Bongo.StoreConnection") def __init__(self, socket): - self.stream = socket.makefile() + # Python 2 returned a byte-like string here. Keep the protocol binary + # under Python 3 so declared payload lengths remain byte lengths. + self.stream = socket.makefile("rwb") def setEventCallback(self, callback): self.eventCallback = callback @@ -102,17 +104,21 @@ class CommandStream: def Read(self, length=-1): return self.stream.read(length) - def Write(self, str, nolog=False): + def Write(self, value, nolog=False): if not nolog: - self.log.debug("SEND: %s", str) - self.stream.write(str) - self.stream.write("\r\n") + self.log.debug("SEND: %s", value) + if not isinstance(value, bytes): + value = value.encode("utf-8") + self.stream.write(value) + self.stream.write(b"\r\n") self.stream.flush() - def WriteRaw(self, str, nolog=False): + def WriteRaw(self, value, nolog=False): if not nolog: - self.log.debug("SEND: %s", str) - self.stream.write(str) + self.log.debug("SEND: %s", value) + if not isinstance(value, bytes): + value = value.encode("utf-8") + self.stream.write(value) self.stream.flush() def GetResponse(self, nolog=False): @@ -136,11 +142,11 @@ class CommandStream: def readline(self): s = self.stream.readline() - if s[-2:] == "\r\n": + if s[-2:] == b"\r\n": s = s[:-2] - elif s[-1:] == "\n": + elif s[-1:] == b"\n": s = s[:-1] - return s + return s.decode("utf-8", "surrogateescape") class ResponseIterator: def __init__(self, stream): @@ -156,7 +162,7 @@ class ResponseIterator: def __next__(self): r = self.stream.GetResponse() if r.code == 1000: - if re.match("^\d+$", r.message): + if re.match(r"^\d+$", r.message): self.total = int(r.message) self.done = True elif r.code > 3000: @@ -201,4 +207,3 @@ class CollectionIterator(ResponseIterator): def __next__(self): collection = Collection(ResponseIterator.next(self)) return collection - diff --git a/src/libs/python/bongo/store/QueueClient.py b/src/libs/python/bongo/store/QueueClient.py index ac2298d..f9c25cf 100644 --- a/src/libs/python/bongo/store/QueueClient.py +++ b/src/libs/python/bongo/store/QueueClient.py @@ -73,8 +73,9 @@ class QueueClient: return r def StoreMessage(self, data): - self.stream.Write("QSTOR MESSAGE %d" % (len(data))) - self.stream.WriteRaw(data) + wire_data = data if isinstance(data, bytes) else data.encode("utf-8") + self.stream.Write("QSTOR MESSAGE %d" % (len(wire_data))) + self.stream.WriteRaw(wire_data) r = self.stream.GetResponse() if r.code != 1000: diff --git a/src/libs/python/bongo/store/StoreClient.py b/src/libs/python/bongo/store/StoreClient.py index c021608..d8f7720 100644 --- a/src/libs/python/bongo/store/StoreClient.py +++ b/src/libs/python/bongo/store/StoreClient.py @@ -95,7 +95,7 @@ class ACL : if entry == '' : continue - tmp = re.split('\s+', entry) + tmp = re.split(r'\s+', entry) self.entries.append((tmp[0], tmp[1], tmp[2])) self.changed = False @@ -408,7 +408,7 @@ class StoreClient: def CookieDelete(self, token): command = "COOKIE DELETE %s" % token - stream.log.debug("SEND: COOKIE DELETE ****************") + self.stream.log.debug("SEND: COOKIE DELETE ****************") self.stream.Write(command, nolog=1) r = self.stream.GetResponse() @@ -417,7 +417,7 @@ class StoreClient: raise CommandError(r) def CookieNew(self, timeout): - command = "COOKIE NEW %d" % (time.time() + timeout) + command = "COOKIE NEW %d" % timeout self.stream.Write(command) r = self.stream.GetResponse(nolog=True) @@ -596,7 +596,7 @@ class StoreClient: raise CommandError(r) def List(self, path, props=[], start=-1, end=-1, flags=None, mask=None): - path = path.replace(" ", "\ ") + path = path.replace(" ", "\\ ") command = "LIST %s" % path if start == -1 and end == -1: @@ -760,7 +760,7 @@ class StoreClient: self.connection.Close() def Read(self, doc): - doc = doc.replace(" ", "\ ") + doc = doc.replace(" ", "\\ ") self.stream.Write("READ %s" % (doc)) r = self.stream.GetResponse() if r.code != 2001: diff --git a/src/libs/python/libbongo/CMakeLists.txt b/src/libs/python/libbongo/CMakeLists.txt index 494c233..fb31ab2 100644 --- a/src/libs/python/libbongo/CMakeLists.txt +++ b/src/libs/python/libbongo/CMakeLists.txt @@ -19,8 +19,15 @@ python_add_module(bootstrap pybongo.c ) +python_add_module(_calendar + calendar-module.c + cal.c + json.c + pybongo.c +) + # remove 'lib' prefix from these libraries. -set_target_properties(libs bootstrap +set_target_properties(libs bootstrap _calendar PROPERTIES PREFIX "" ) @@ -35,8 +42,18 @@ target_link_libraries(libs ) target_link_libraries(bootstrap + bongomsgapi ${PYTHON_LIBRARIES} ) -install(TARGETS libs bootstrap DESTINATION ${PYTHON_SITELIB_PATH}/libbongo/) +target_link_libraries(_calendar + bongomsgapi + bongojson + bongoutil + bongoxpl + bongocal + ${PYTHON_LIBRARIES} +) + +install(TARGETS libs bootstrap _calendar DESTINATION ${PYTHON_SITELIB_PATH}/libbongo/) install(FILES __init__.py DESTINATION ${PYTHON_SITELIB_PATH}/libbongo/) diff --git a/src/libs/python/libbongo/bongoutil.c b/src/libs/python/libbongo/bongoutil.c index 426ca92..285a082 100755 --- a/src/libs/python/libbongo/bongoutil.c +++ b/src/libs/python/libbongo/bongoutil.c @@ -41,9 +41,11 @@ static PyObject * bongoutil_ModifiedUtf7ToUtf8(PyObject *self, PyObject *args) { char *utf7; - unsigned int utf7Len; + size_t utf7Len; char utf8[XPL_MAX_PATH]; - unsigned int utf8Len; + int utf8Len; + + (void)self; if (!PyArg_ParseTuple(args, "s", &utf7)) { return NULL; @@ -52,7 +54,7 @@ bongoutil_ModifiedUtf7ToUtf8(PyObject *self, PyObject *args) utf7Len = strlen(utf7); utf8Len = ModifiedUtf7ToUtf8(utf7, utf7Len, utf8, XPL_MAX_PATH); - if(utf8Len < 0) { + if (utf8Len < 0 || utf8Len >= XPL_MAX_PATH) { Py_INCREF(Py_None); return Py_None; } diff --git a/src/libs/python/libbongo/bootstrap.c b/src/libs/python/libbongo/bootstrap.c index 01219d0..875e7ec 100644 --- a/src/libs/python/libbongo/bootstrap.c +++ b/src/libs/python/libbongo/bootstrap.c @@ -30,7 +30,6 @@ #include #include "bootstrap.h" #include "msgapi-defs.h" -#include #include #ifdef __cplusplus @@ -38,6 +37,17 @@ extern "C" { #endif static PyMethodDef ModuleMethods[] = { {NULL} }; +static struct PyModuleDef Module = { + .m_base = PyModuleDef_HEAD_INIT, + .m_name = "bootstrap", + .m_doc = NULL, + .m_size = -1, + .m_methods = ModuleMethods, + .m_slots = NULL, + .m_traverse = NULL, + .m_clear = NULL, + .m_free = NULL +}; extern EnumItemDef MdbEnums[]; extern PyMethodDef MdbMethods[]; extern EnumItemDef MsgApiEnums[]; @@ -50,22 +60,19 @@ PyMethodDef MsgApiMethods[] = { }; PyMODINIT_FUNC -initbootstrap() +PyInit_bootstrap(void) { - char dbfdir[PATH_MAX]; - - /* Initialize the various bongo libraries */ - if (!MemoryManagerOpen((unsigned char*)"pybongo")) { - PyErr_SetString(PyExc_ImportError, - "bongo.libs error: MemoryManagerOpen() failed"); - return; - } + PyObject *module; /* Create the libs module */ - PyObject *module = Py_InitModule("bootstrap", ModuleMethods); + module = PyModule_Create(&Module); + if (!module) { + return NULL; + } /* Add the Bongo libs */ AddLibrary(module, "msgapi", MsgApiMethods, MsgApiEnums); + return module; } #ifdef __cplusplus diff --git a/src/libs/python/libbongo/cal.c b/src/libs/python/libbongo/cal.c index 2401f58..8a3f664 100644 --- a/src/libs/python/libbongo/cal.c +++ b/src/libs/python/libbongo/cal.c @@ -17,6 +17,8 @@ cal_Merge(PyObject *self, PyObject *args) JsonObject *a; JsonObject *b; JsonObject *ret; + + (void)self; if (!PyArg_ParseTuple(args, "OO", &a, &b)) { PyErr_SetString(PyExc_TypeError, "Merge() takes 2 arguments"); @@ -79,6 +81,7 @@ CalTimeFromPy(PyObject *dt) static PyObject * cal_PrimaryOccurrence(PyObject *self, PyObject *args) { + (void)self; JsonObject *json; const char *tzid; BongoCalObject *cal; @@ -109,6 +112,7 @@ cal_PrimaryOccurrence(PyObject *self, PyObject *args) static PyObject * cal_Collect(PyObject *self, PyObject *args) { + (void)self; JsonObject *json; BongoCalObject *cal; GArray *occs; @@ -162,6 +166,7 @@ cal_Collect(PyObject *self, PyObject *args) static PyObject * cal_JsonToIcal(PyObject *self, PyObject *args) { + (void)self; JsonObject *a; BongoCalObject *cal; icalcomponent *comp; @@ -192,6 +197,7 @@ cal_JsonToIcal(PyObject *self, PyObject *args) static PyObject * cal_IcalToJson(PyObject *self, PyObject *args) { + (void)self; BongoJsonObject *json; icalcomponent *comp; char *str; @@ -202,15 +208,26 @@ cal_IcalToJson(PyObject *self, PyObject *args) } comp = icalparser_parse_string(str); + if (comp == NULL) { + PyErr_SetString(PyExc_ValueError, "Invalid iCalendar data"); + return NULL; + } json = BongoCalIcalToJson(comp); icalcomponent_free(comp); + if (json == NULL) { + PyErr_SetString(PyExc_ValueError, + "Unsupported or invalid iCalendar component"); + return NULL; + } + return JsonObjectToPy(json, TRUE); } static PyObject * cal_BongoCalTimeToStringConcise(PyObject *self, PyObject *args) { + (void)self; BongoCalTime t; char *str; char buf[128]; @@ -257,6 +274,7 @@ GetTz(const char *tzid, BongoCalObject **cal, const char *json) static PyObject * cal_ConvertTzid(PyObject *self, PyObject *args) { + (void)self; BongoCalTime t; const char *json; const char *timestr; @@ -305,6 +323,7 @@ cal_ConvertTzid(PyObject *self, PyObject *args) int BongoCalPostInit(PyObject *module) { + (void)module; #ifdef PyDateTime_IMPORT PyDateTime_IMPORT; #endif @@ -313,13 +332,13 @@ BongoCalPostInit(PyObject *module) } PyMethodDef CalMethods[] = { - {"Merge", cal_Merge, METH_VARARGS | METH_STATIC}, - {"Collect", cal_Collect, METH_VARARGS | METH_STATIC}, - {"IcalToJson", cal_IcalToJson, METH_VARARGS | METH_STATIC}, - {"JsonToIcal", cal_JsonToIcal, METH_VARARGS | METH_STATIC}, - {"PrimaryOccurrence", cal_PrimaryOccurrence, METH_VARARGS | METH_STATIC}, - {"TimeToStringConcise", cal_BongoCalTimeToStringConcise, METH_VARARGS | METH_STATIC }, - {"ConvertTzid", cal_ConvertTzid, METH_VARARGS | METH_STATIC }, + {"Merge", cal_Merge, METH_VARARGS | METH_STATIC, NULL}, + {"Collect", cal_Collect, METH_VARARGS | METH_STATIC, NULL}, + {"IcalToJson", cal_IcalToJson, METH_VARARGS | METH_STATIC, NULL}, + {"JsonToIcal", cal_JsonToIcal, METH_VARARGS | METH_STATIC, NULL}, + {"PrimaryOccurrence", cal_PrimaryOccurrence, METH_VARARGS | METH_STATIC, NULL}, + {"TimeToStringConcise", cal_BongoCalTimeToStringConcise, METH_VARARGS | METH_STATIC, NULL}, + {"ConvertTzid", cal_ConvertTzid, METH_VARARGS | METH_STATIC, NULL}, {NULL, NULL, 0, NULL} }; diff --git a/src/libs/python/libbongo/calendar-module.c b/src/libs/python/libbongo/calendar-module.c new file mode 100644 index 0000000..af5a447 --- /dev/null +++ b/src/libs/python/libbongo/calendar-module.c @@ -0,0 +1,49 @@ +#include +#include +#include +#include + +#include "libs.h" + +extern PyMethodDef CalMethods[]; +extern PyMethodDef BongoJsonMethods[]; + +static PyMethodDef ModuleMethods[] = {{NULL}}; + +static struct PyModuleDef Module = { + .m_base = PyModuleDef_HEAD_INIT, + .m_name = "_calendar", + .m_doc = "Standalone Bongo iCalendar conversion bindings.", + .m_size = -1, + .m_methods = ModuleMethods, + .m_slots = NULL, + .m_traverse = NULL, + .m_clear = NULL, + .m_free = NULL +}; + +PyMODINIT_FUNC +PyInit__calendar(void) +{ + PyObject *module; + + XplInit(); + if (!BongoJsonPreInit()) { + return NULL; + } + if (!BongoCalInit(XPL_DEFAULT_DBF_DIR)) { + PyErr_SetString(PyExc_ImportError, + "could not initialize Bongo calendar timezones"); + return NULL; + } + + module = PyModule_Create(&Module); + if (module == NULL) { + return NULL; + } + AddLibrary(module, "cal", CalMethods, NULL); + AddLibrary(module, "bongojson", BongoJsonMethods, NULL); + BongoJsonPostInit(module); + BongoCalPostInit(module); + return module; +} diff --git a/src/libs/python/libbongo/json.c b/src/libs/python/libbongo/json.c index d2c6d82..366fa4f 100644 --- a/src/libs/python/libbongo/json.c +++ b/src/libs/python/libbongo/json.c @@ -17,51 +17,24 @@ static PyMethodDef JsonObject_methods[] = { }; static PyMappingMethods JsonObject_mapping = { - JsonObject_length, - JsonObject_subscript, - JsonObject_subscript_assign + .mp_length = JsonObject_length, + .mp_subscript = JsonObject_subscript, + .mp_ass_subscript = JsonObject_subscript_assign }; PyTypeObject JsonObjectType = { - PyObject_HEAD_INIT(NULL) - 0, /* ob_size */ - "libs.JsonObject", - sizeof(JsonObject), - 0, /*tp_itemsize*/ - (destructor)JsonObject_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - 0, /*tp_as_sequence*/ - &JsonObject_mapping, /*tp_as_mapping*/ - 0, /*tp_hash */ - 0, /*tp_call*/ - JsonObject_str, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ - "Bongo Json Object", /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - JsonObject_methods, /* tp_methods */ - JsonObject_members, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - (initproc)JsonObject_init, /* tp_init */ - 0, /* tp_alloc */ - JsonObject_new, /* tp_new */ + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "libs.JsonObject", + .tp_basicsize = sizeof(JsonObject), + .tp_dealloc = (destructor)JsonObject_dealloc, + .tp_as_mapping = &JsonObject_mapping, + .tp_str = JsonObject_str, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_doc = "Bongo Json Object", + .tp_methods = JsonObject_methods, + .tp_members = JsonObject_members, + .tp_init = (initproc)JsonObject_init, + .tp_new = JsonObject_new, }; @@ -75,16 +48,8 @@ static PyMethodDef JsonArray_methods[] = { }; static PySequenceMethods JsonArray_sequence = { - JsonArray_length, - JsonArray_concat, - JsonArray_repeat, - JsonArray_item, - JsonArray_slice, - JsonArray_assign, - JsonArray_assign_slice, - JsonArray_contains, - JsonArray_inplace_concat, - JsonArray_inplace_repeat, + .sq_length = JsonArray_length, + .sq_item = JsonArray_item, }; #if 0 @@ -103,45 +68,18 @@ static PySequenceMethods JsonArray_sequence = { PyTypeObject JsonArrayType = { - PyObject_HEAD_INIT(NULL) - 0, /* ob_size */ - "libs.JsonArray", - sizeof(JsonArray), - 0, /*tp_itemsize*/ - (destructor)JsonArray_dealloc, /*tp_dealloc*/ - 0, /*tp_print*/ - 0, /*tp_getattr*/ - 0, /*tp_setattr*/ - 0, /*tp_compare*/ - 0, /*tp_repr*/ - 0, /*tp_as_number*/ - &JsonArray_sequence, /*tp_as_sequence*/ - 0, /*tp_as_mapping*/ - 0, /*tp_hash */ - 0, /*tp_call*/ - JsonArray_str, /*tp_str*/ - 0, /*tp_getattro*/ - 0, /*tp_setattro*/ - 0, /*tp_as_buffer*/ - Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /*tp_flags*/ - "Bongo Json Array", /* tp_doc */ - 0, /* tp_traverse */ - 0, /* tp_clear */ - 0, /* tp_richcompare */ - 0, /* tp_weaklistoffset */ - 0, /* tp_iter */ - 0, /* tp_iternext */ - JsonArray_methods, /* tp_methods */ - JsonArray_members, /* tp_members */ - 0, /* tp_getset */ - 0, /* tp_base */ - 0, /* tp_dict */ - 0, /* tp_descr_get */ - 0, /* tp_descr_set */ - 0, /* tp_dictoffset */ - (initproc)JsonArray_init, /* tp_init */ - 0, /* tp_alloc */ - JsonArray_new, /* tp_new */ + PyVarObject_HEAD_INIT(NULL, 0) + .tp_name = "libs.JsonArray", + .tp_basicsize = sizeof(JsonArray), + .tp_dealloc = (destructor)JsonArray_dealloc, + .tp_as_sequence = &JsonArray_sequence, + .tp_str = JsonArray_str, + .tp_flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, + .tp_doc = "Bongo Json Array", + .tp_methods = JsonArray_methods, + .tp_members = JsonArray_members, + .tp_init = (initproc)JsonArray_init, + .tp_new = JsonArray_new, }; static void @@ -291,7 +229,8 @@ JsonNodeToPy(BongoJsonNode *node, BOOL own, BOOL native) ret = (PyObject*)PyObject_New(JsonArray, &JsonArrayType); ((JsonArray*)ret)->own = own; ((JsonArray*)ret)->array = BongoJsonNodeAsArray(node); - } + } + break; case BONGO_JSON_BOOL : if (BongoJsonNodeAsBool(node)) { ret = Py_True; @@ -327,13 +266,16 @@ JsonArray_dealloc(JsonArray *self) BongoJsonArrayFree(self->array); } - self->ob_type->tp_free((PyObject*)self); + Py_TYPE(self)->tp_free((PyObject*)self); } PyObject * JsonArray_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { JsonArray *self; + + (void)args; + (void)kwds; self = (JsonArray*)type->tp_alloc(type, 0); self->own = FALSE; @@ -348,6 +290,8 @@ JsonArray_new(PyTypeObject *type, PyObject *args, PyObject *kwds) int JsonArray_init(JsonArray *self, PyObject *args, PyObject *kwds) { + (void)args; + (void)kwds; self->own = TRUE; self->array = g_array_sized_new(FALSE, FALSE, sizeof(BongoJsonNode*), 16); return 0; @@ -400,20 +344,6 @@ JsonArray_length(PyObject *selfp) return (self->array->len > 0) ? (Py_ssize_t)self->array->len : 0; } -PyObject * -JsonArray_concat(PyObject *selfp, PyObject *b) -{ - PyErr_SetString(PyExc_NotImplementedError, "Not implemented"); - return NULL; -} - -PyObject * -JsonArray_repeat(PyObject *selfp, Py_ssize_t i) -{ - PyErr_SetString(PyExc_NotImplementedError, "Not implemented"); - return NULL; -} - PyObject * JsonArray_item(PyObject *selfp, Py_ssize_t i) { @@ -430,48 +360,6 @@ JsonArray_item(PyObject *selfp, Py_ssize_t i) return JsonNodeToPy(node, FALSE, FALSE); } -PyObject * -JsonArray_slice(PyObject *selfp, Py_ssize_t i1, Py_ssize_t i2) -{ - PyErr_SetString(PyExc_NotImplementedError, "Not implemented"); - return NULL; -} - -int -JsonArray_assign(PyObject *selfp, Py_ssize_t i, PyObject *obj) -{ - PyErr_SetString(PyExc_NotImplementedError, "Not implemented"); - return 0; -} - -int -JsonArray_assign_slice(PyObject *selfp, Py_ssize_t i1, Py_ssize_t i2, PyObject *obj) -{ - PyErr_SetString(PyExc_NotImplementedError, "Not implemented"); - return 0; -} - -int -JsonArray_contains(PyObject *selfp, PyObject *b) -{ - PyErr_SetString(PyExc_NotImplementedError, "Not implemented"); - return 0; -} - -PyObject * -JsonArray_inplace_concat(PyObject *selfp, PyObject *b) -{ - PyErr_SetString(PyExc_NotImplementedError, "Not implemented"); - return NULL; -} - -PyObject * -JsonArray_inplace_repeat(PyObject *selfp, Py_ssize_t i) -{ - PyErr_SetString(PyExc_NotImplementedError, "Not implemented"); - return NULL; -} - /*** JsonObject ***/ void @@ -483,7 +371,7 @@ JsonObject_dealloc(JsonObject *self) } #endif - self->ob_type->tp_free((PyObject*)self); + Py_TYPE(self)->tp_free((PyObject*)self); } @@ -492,6 +380,9 @@ JsonObject_new(PyTypeObject *type, PyObject *args, PyObject *kwds) { JsonObject *self; + (void)args; + (void)kwds; + self = (JsonObject*)type->tp_alloc(type, 0); self->obj = NULL; self->own = FALSE; @@ -506,6 +397,8 @@ JsonObject_new(PyTypeObject *type, PyObject *args, PyObject *kwds) int JsonObject_init(JsonObject *self, PyObject *args, PyObject *kwds) { + (void)args; + (void)kwds; self->obj = BongoJsonObjectNew(); self->own = TRUE; self->valid = TRUE; @@ -531,6 +424,7 @@ JsonObject_str(PyObject *selfp) Py_ssize_t JsonObject_length(PyObject *self) { + (void)self; /* FIXME */ return 1; } @@ -618,6 +512,7 @@ BongoJsonPostInit(PyObject *module) static PyObject * bongojson_BongoJsonParseString(PyObject *self, PyObject *args) { + (void)self; char *jsonStr; BongoJsonNode *node; BongoJsonResult res; @@ -665,6 +560,7 @@ bongojson_BongoJsonParseString(PyObject *self, PyObject *args) static PyObject * bongojson_ToPy(PyObject *self, PyObject *args) { + (void)self; PyObject *obj; PyObject *ret; diff --git a/src/libs/python/libbongo/json.h b/src/libs/python/libbongo/json.h index aaaea0c..e2c9c24 100644 --- a/src/libs/python/libbongo/json.h +++ b/src/libs/python/libbongo/json.h @@ -7,11 +7,6 @@ #include #include -// attempt to support python 2.4-2.5 -#ifndef Py_ssize_t -#define Py_ssize_t int -#endif - #ifdef __cplusplus extern "C" { #endif @@ -65,15 +60,7 @@ extern PyTypeObject JsonArrayType; /* Sequence protocol */ Py_ssize_t JsonArray_length(PyObject *selfp); -PyObject *JsonArray_concat(PyObject *selfp, PyObject *b); -PyObject *JsonArray_repeat(PyObject *selfp, Py_ssize_t i); PyObject *JsonArray_item(PyObject *selfp, Py_ssize_t i); -PyObject *JsonArray_slice(PyObject *selfp, Py_ssize_t i1, Py_ssize_t i2); -int JsonArray_assign(PyObject *selfp, Py_ssize_t i, PyObject *obj); -int JsonArray_assign_slice(PyObject *selfp, Py_ssize_t i1, Py_ssize_t i2, PyObject *obj); -int JsonArray_contains(PyObject *selfp, PyObject *b); -PyObject *JsonArray_inplace_concat(PyObject *selfp, PyObject *b); -PyObject *JsonArray_inplace_repeat(PyObject *selfp, Py_ssize_t i); PyObject *JsonObjectToPy(BongoJsonObject *object, BOOL own); diff --git a/src/libs/python/libbongo/libs.c b/src/libs/python/libbongo/libs.c index 3b18513..fc8d286 100644 --- a/src/libs/python/libbongo/libs.c +++ b/src/libs/python/libbongo/libs.c @@ -36,6 +36,17 @@ extern "C" { #endif static PyMethodDef ModuleMethods[] = { {NULL} }; +static struct PyModuleDef Module = { + .m_base = PyModuleDef_HEAD_INIT, + .m_name = "libs", + .m_doc = NULL, + .m_size = -1, + .m_methods = ModuleMethods, + .m_slots = NULL, + .m_traverse = NULL, + .m_clear = NULL, + .m_free = NULL +}; /* Pull in the methods from the bindings */ #if 0 @@ -54,15 +65,15 @@ extern PyMethodDef BongoUtilMethods[]; extern PyMethodDef TestMethods[]; PyMODINIT_FUNC -initlibs() +PyInit_libs(void) { - char dbfdir[PATH_MAX]; + PyObject *module; /* Initialize the various bongo libraries */ if (!ConnStartup(DEFAULT_CONNECTION_TIMEOUT)) { PyErr_SetString(PyExc_ImportError, "bongo.libs error: ConnStartup() failed"); - return; + return NULL; } MsgInit(); @@ -70,29 +81,32 @@ initlibs() if (!BongoCalInit(MsgGetDir(MSGAPI_DIR_DBF, NULL, 0))) { PyErr_SetString(PyExc_ImportError, "bongo.libs error: BongoCalInit() failed"); - return; + return NULL; } if (!NMAPInitialize()) { PyErr_SetString(PyExc_ImportError, "bongo.libs error: NMAPInitialize() failed"); - return; + return NULL; } if (!StreamioInit()) { PyErr_SetString(PyExc_ImportError, "bongo.libs error: StreamioInit() failed"); - return; + return NULL; } if (!BongoJsonPreInit()) { PyErr_SetString(PyExc_ImportError, "bongo.libs error: BongoJsonPreInit() failed"); - return; + return NULL; } /* Create the libs module */ - PyObject *module = Py_InitModule("libs", ModuleMethods); + module = PyModule_Create(&Module); + if (!module) { + return NULL; + } /* Add the Bongo libs */ AddLibrary(module, "cal", CalMethods, NULL); @@ -105,6 +119,7 @@ initlibs() BongoJsonPostInit(module); BongoCalPostInit(module); + return module; } #ifdef __cplusplus diff --git a/src/libs/python/libbongo/msgapi-defs.c b/src/libs/python/libbongo/msgapi-defs.c index f61e037..a149ff7 100644 --- a/src/libs/python/libbongo/msgapi-defs.c +++ b/src/libs/python/libbongo/msgapi-defs.c @@ -1,8 +1,36 @@ #include #include "msgapi-defs.h" +PyObject * +msgapi_GetConfigProperty(PyObject *self, PyObject *args) +{ + const char *property; + unsigned char value[MSGSRV_CONFIG_MAX_PROP_CHARS + 1]; + + (void)self; + if (!PyArg_ParseTuple(args, "s", &property)) + return NULL; + if (!MsgGetConfigProperty(value, (unsigned char *)property)) { + PyErr_SetString(PyExc_KeyError, property); + return NULL; + } + return PyUnicode_FromString((const char *)value); +} + PyObject * msgapi_GetUnprivilegedUser(PyObject *self, PyObject *args) { + (void)self; + (void)args; return Py_BuildValue("z", MsgGetUnprivilegedUser()); } + +EnumItemDef MsgApiEnums[] = { + {"MAX_PROP_CHARS", MSGSRV_CONFIG_MAX_PROP_CHARS, NULL}, + {"MESSAGING_SERVER", 0, MSGSRV_CONFIG_PROP_MESSAGING_SERVER}, + {"WEB_ADMIN_SERVER", 0, MSGSRV_CONFIG_PROP_WEB_ADMIN_SERVER}, + {"DEFAULT_CONTEXT", 0, MSGSRV_CONFIG_PROP_DEFAULT_CONTEXT}, + {"BONGO_SERVICES", 0, MSGSRV_CONFIG_PROP_BONGO_SERVICES}, + {"CONFIG_FILENAME", 0, MSGSRV_CONFIG_FILENAME}, + {NULL, 0, NULL} +}; diff --git a/src/libs/python/libbongo/msgapi.c b/src/libs/python/libbongo/msgapi.c index b2f57c2..199573a 100644 --- a/src/libs/python/libbongo/msgapi.c +++ b/src/libs/python/libbongo/msgapi.c @@ -21,6 +21,7 @@ ****************************************************************************/ #include +#include #include #include @@ -41,6 +42,7 @@ Collect and import the calendars subscribed to by all users."); static PyObject * msgapi_CollectAllUsers(PyObject *self, PyObject *args) { + (void)self; if (!PyArg_ParseTuple(args, "")) { return NULL; } @@ -61,6 +63,8 @@ msgapi_CollectUser(PyObject *self, PyObject *args) { char *user; + (void)self; + if (!PyArg_ParseTuple(args, "s", &user)) { return NULL; } @@ -71,38 +75,6 @@ msgapi_CollectUser(PyObject *self, PyObject *args) return Py_None; } -PyDoc_STRVAR(GetUserFeature_doc, -"GetUserFeature(dn, featurename, ) -> array of strings\n\ -\n\ -Return a string containing the ip address of user u's document store."); - -static const char *features[] = { - "", - "imap", - "pop", - "addressbook", - "proxy", - "forward", - "autoreply", - "rules", - "finger", - "smtp_send_count_limit", - "smtp_send_size_limit", - "nmap_quota", - "nmap_store", - "webmail", - "modweb", - "mwmail_addressbook_personal", - "mwmail_addressbook_system", - "mwmail_addressbook_global", - "modweb_wap", - "calagent", - "calendar", - "antivirus", - "shared_folders", - NULL -}; - static void ThrowImportException(int code) { @@ -140,6 +112,7 @@ The calendar with that name must already exist."); static PyObject * msgapi_ImportIcsUrl(PyObject *self, PyObject *args) { + (void)self; char *user; char *calName; char *url; @@ -170,6 +143,7 @@ The calendar with that name must already exist."); static PyObject * msgapi_ImportIcs(PyObject *self, PyObject *args) { + (void)self; PyObject *pyfile = NULL; FILE *file = NULL; char *user; @@ -180,12 +154,22 @@ msgapi_ImportIcs(PyObject *self, PyObject *args) if (!PyArg_ParseTuple(args, "Ossz", &pyfile, &user, &calName, &color)) { return NULL; } - if(!PyFile_Check(pyfile)) + int fd = PyObject_AsFileDescriptor(pyfile); + if (fd < 0) return NULL; - file = PyFile_AsFile(pyfile); + fd = dup(fd); + if (fd < 0) { + return PyErr_SetFromErrno(PyExc_OSError); + } + file = fdopen(fd, "rb"); + if (!file) { + close(fd); + return PyErr_SetFromErrno(PyExc_OSError); + } ret = MsgImportIcs(file, user, calName, color, NULL); + fclose(file); if (ret < 0) { ThrowImportException(ret); return NULL; @@ -202,6 +186,7 @@ Import the specified calendar url."); static PyObject * msgapi_IcsImport(PyObject *self, PyObject *args) { + (void)self; char *user; char *name; char *url; @@ -233,6 +218,7 @@ Subscribe user to the specified calendar url."); static PyObject * msgapi_IcsSubscribe(PyObject *self, PyObject *args) { + (void)self; char *user; char *name; char *url; @@ -265,6 +251,7 @@ s is of the format \"NMAP <40c0cbb0hostname441444a4>\"."); static PyObject * msgapi_NmapChallenge(PyObject *self, PyObject *args) { + (void)self; unsigned char *salt; unsigned int len=33; unsigned char buf[len]; @@ -290,6 +277,9 @@ msgapi_GetBuildVersion(PyObject *self, PyObject *args) int version; BOOL custom; + (void)self; + (void)args; + MsgGetBuildVersion (&version, &custom); return Py_BuildValue("ib", version, custom); @@ -306,6 +296,9 @@ msgapi_GetAvailableVersion(PyObject *self, PyObject *args) { int version; + (void)self; + (void)args; + if (MsgGetAvailableVersion (&version)) { return Py_BuildValue("i", version); } else { diff --git a/src/libs/python/libbongo/pybongo.c b/src/libs/python/libbongo/pybongo.c index 8c69829..07ce05a 100644 --- a/src/libs/python/libbongo/pybongo.c +++ b/src/libs/python/libbongo/pybongo.c @@ -11,26 +11,22 @@ AddLibrary(PyObject *module, char *name, PyMethodDef *classMethods, PyMethodDef *def; EnumItemDef *item; - /* Create the new class object and add it to the module */ - PyObject *moduleDict = PyModule_GetDict(module); - PyObject *classDict = PyDict_New(); - PyObject *className = PyString_FromString(name); - PyObject *classObj = PyClass_New(NULL, classDict, className); + PyObject *library = PyModule_New(name); - PyDict_SetItemString(moduleDict, name, classObj); + if (!library) { + return; + } /* Add the class's methods to its dict */ for (def = classMethods; def->ml_name != NULL; def++) { - PyObject *func = PyCFunction_New(def, NULL); - PyObject *method; - if (def->ml_flags & METH_STATIC) { - method = PyStaticMethod_New(func); - } else { - method = PyMethod_New(func, NULL, classObj); + PyObject *func; + def->ml_flags &= ~METH_STATIC; + func = PyCFunction_NewEx(def, NULL, library); + if (!func || PyModule_AddObject(library, def->ml_name, func) < 0) { + Py_XDECREF(func); + Py_DECREF(library); + return; } - PyDict_SetItemString(classDict, def->ml_name, method); - Py_DECREF(func); - Py_DECREF(method); } if (enumItems != NULL) { @@ -41,14 +37,17 @@ AddLibrary(PyObject *module, char *name, PyMethodDef *classMethods, } else { value = PyInt_FromLong(item->value); } - PyDict_SetItemString(classDict, item->name, value); - Py_DECREF(value); + if (!value || PyModule_AddObject(library, item->name, value) < 0) { + Py_XDECREF(value); + Py_DECREF(library); + return; + } } } - Py_DECREF(classDict); - Py_DECREF(className); - Py_DECREF(classObj); + if (PyModule_AddObject(module, name, library) < 0) { + Py_DECREF(library); + } } #ifdef __cplusplus diff --git a/src/libs/python/libbongo/pybongo.h b/src/libs/python/libbongo/pybongo.h index 9860908..c8b0f78 100644 --- a/src/libs/python/libbongo/pybongo.h +++ b/src/libs/python/libbongo/pybongo.h @@ -3,6 +3,13 @@ #include +#define PyInt_Check PyLong_Check +#define PyInt_AsLong PyLong_AsLong +#define PyInt_FromLong PyLong_FromLong +#define PyString_Check PyUnicode_Check +#define PyString_AsString(object) ((char *)PyUnicode_AsUTF8(object)) +#define PyString_FromString PyUnicode_FromString + #ifdef __cplusplus extern "C" { #endif diff --git a/src/libs/python/libbongo/streamio.c b/src/libs/python/libbongo/streamio.c index 8417895..036563e 100644 --- a/src/libs/python/libbongo/streamio.c +++ b/src/libs/python/libbongo/streamio.c @@ -2,6 +2,16 @@ #include #include +#include "pybongo.h" + +static const char *BONGO_STREAM_CAPSULE = "bongo.BongoStream"; + +static void +BongoStreamCapsuleFree(PyObject *capsule) +{ + BongoStream *stream = PyCapsule_GetPointer(capsule, BONGO_STREAM_CAPSULE); + if (stream) BongoStreamFree(stream); +} #ifdef __cplusplus extern "C" { @@ -15,6 +25,7 @@ Add a codec to a stream."); static PyObject * streamio_BongoStreamAddCodec(PyObject *self, PyObject *args) { + (void)self; PyObject *pystream = NULL; BongoStream *stream = NULL; char *codec = NULL; @@ -25,7 +36,8 @@ streamio_BongoStreamAddCodec(PyObject *self, PyObject *args) if (!PyArg_ParseTuple(args, "OsO", &pystream, &codec, &pyencode)) { return NULL; } - stream = PyCObject_AsVoidPtr(pystream); + stream = PyCapsule_GetPointer(pystream, BONGO_STREAM_CAPSULE); + if (!stream) return NULL; if (!PyBool_Check(pyencode)) { PyErr_SetString(PyExc_TypeError, @@ -50,6 +62,7 @@ Return a pointer to a BongoStream."); static PyObject * streamio_BongoStreamCreate(PyObject *self, PyObject *args) { + (void)self; PyObject *pycodecs = NULL; PyObject *pyencode = NULL; BongoStream *stream = NULL; @@ -89,7 +102,7 @@ streamio_BongoStreamCreate(PyObject *self, PyObject *args) return Py_None; } - return PyCObject_FromVoidPtr(stream, (void *)BongoStreamFree); + return PyCapsule_New(stream, BONGO_STREAM_CAPSULE, BongoStreamCapsuleFree); } PyDoc_STRVAR(BongoStreamEos_doc, @@ -100,13 +113,15 @@ Close down a stream."); static PyObject * streamio_BongoStreamEos(PyObject *self, PyObject *args) { + (void)self; PyObject *pystream = NULL; BongoStream *stream = NULL; if (!PyArg_ParseTuple(args, "O", &pystream)) { return NULL; } - stream = PyCObject_AsVoidPtr(pystream); + stream = PyCapsule_GetPointer(pystream, BONGO_STREAM_CAPSULE); + if (!stream) return NULL; BongoStreamEos(stream); @@ -123,25 +138,27 @@ Return the number of bytes read."); static PyObject * streamio_BongoStreamGet(PyObject *self, PyObject *args) { + (void)self; PyObject *pystream = NULL; BongoStream *stream = NULL; PyObject *pybuf = NULL; - char *buf; - int bufLen; + Py_buffer view; int read = 0; if (!PyArg_ParseTuple(args, "OO", &pystream, &pybuf)) { return NULL; } - stream = PyCObject_AsVoidPtr(pystream); + stream = PyCapsule_GetPointer(pystream, BONGO_STREAM_CAPSULE); + if (!stream) return NULL; - if (PyObject_AsCharBuffer(pybuf, (const char**)&buf, &bufLen)) { - PyErr_SetString(PyExc_TypeError, "BongoStreamGet() buffer must be a string"); + if (PyObject_GetBuffer(pybuf, &view, PyBUF_WRITABLE) < 0) { + PyErr_SetString(PyExc_TypeError, "BongoStreamGet() buffer must be writable"); return NULL; } - read = BongoStreamGet(stream, buf, bufLen); + read = BongoStreamGet(stream, view.buf, view.len); + PyBuffer_Release(&view); return Py_BuildValue("i", read); } @@ -154,24 +171,26 @@ Put as many bytes possible from buffer into the stream."); static PyObject * streamio_BongoStreamPut(PyObject *self, PyObject *args) { + (void)self; PyObject *pystream = NULL; BongoStream *stream = NULL; PyObject *pybuf = NULL; - const char *buf; - int bufLen; + Py_buffer view; if (!PyArg_ParseTuple(args, "OO", &pystream, &pybuf)) { return NULL; } - stream = PyCObject_AsVoidPtr(pystream); + stream = PyCapsule_GetPointer(pystream, BONGO_STREAM_CAPSULE); + if (!stream) return NULL; - if (PyObject_AsCharBuffer(pybuf, &buf, &bufLen)) { - PyErr_SetString(PyExc_TypeError, "BongoStreamPut() buffer must be a string"); + if (PyObject_GetBuffer(pybuf, &view, PyBUF_SIMPLE) < 0) { + PyErr_SetString(PyExc_TypeError, "BongoStreamPut() argument must support the buffer protocol"); return NULL; } - BongoStreamPut(stream, buf, bufLen); + BongoStreamPut(stream, view.buf, view.len); + PyBuffer_Release(&view); Py_INCREF(Py_None); return Py_None; diff --git a/src/libs/python/libbongo/tests.c b/src/libs/python/libbongo/tests.c index 0d3ab78..31b959c 100644 --- a/src/libs/python/libbongo/tests.c +++ b/src/libs/python/libbongo/tests.c @@ -14,6 +14,8 @@ extern "C" { static PyObject * test_GetAsciiString(PyObject *one, PyObject *two) { + (void)one; + (void)two; char *hello_english = "Hello"; PyObject *ret; @@ -25,6 +27,8 @@ test_GetAsciiString(PyObject *one, PyObject *two) static PyObject * test_GetUnicodeString(PyObject *one, PyObject *two) { + (void)one; + (void)two; char *hello_finnish = "hyvää päivää"; PyObject *ret; @@ -35,8 +39,8 @@ test_GetUnicodeString(PyObject *one, PyObject *two) } PyMethodDef TestMethods[] = { - {"GetAsciiString", test_GetAsciiString, METH_VARARGS | METH_STATIC}, - {"GetUnicodeString", test_GetUnicodeString, METH_VARARGS | METH_STATIC}, + {"GetAsciiString", test_GetAsciiString, METH_VARARGS | METH_STATIC, NULL}, + {"GetUnicodeString", test_GetUnicodeString, METH_VARARGS | METH_STATIC, NULL}, {NULL, NULL, 0, NULL} }; diff --git a/src/libs/sasl/CMakeLists.txt b/src/libs/sasl/CMakeLists.txt new file mode 100644 index 0000000..7898558 --- /dev/null +++ b/src/libs/sasl/CMakeLists.txt @@ -0,0 +1,10 @@ +add_library(bongosasl sasl.c) +target_include_directories(bongosasl PRIVATE ${SASL2_INCLUDE_DIRS}) +target_link_libraries(bongosasl PRIVATE ${SASL2_LIBRARIES}) +install(TARGETS bongosasl DESTINATION ${LIB_INSTALL_DIR}) + +if(BUILD_TESTING) + add_executable(bongo-sasl-test tests/sasl-test.c) + target_link_libraries(bongo-sasl-test PRIVATE bongosasl) + add_test(NAME sasl-plain COMMAND bongo-sasl-test) +endif() diff --git a/src/libs/sasl/sasl.c b/src/libs/sasl/sasl.c new file mode 100644 index 0000000..84639f9 --- /dev/null +++ b/src/libs/sasl/sasl.c @@ -0,0 +1,177 @@ +#include +#include +#include +#include +#include +#include + +struct BongoSaslSession { + sasl_conn_t *connection; + BongoSaslVerify verify; + void *context; + sasl_callback_t callbacks[2]; + char mechanisms[256]; +}; + +static int +check_password(sasl_conn_t *connection, void *context, const char *user, + const char *password, unsigned password_length, + struct propctx *properties) +{ + BongoSaslSession *session = context; + (void)connection; + (void)properties; + if (!session || !session->verify || !user || !password) return SASL_BADPARAM; + return session->verify(session->context, user, password, password_length) + ? SASL_OK : SASL_BADAUTH; +} + +int BongoSaslInitialize(void) +{ + return sasl_server_init(NULL, "bongo") == SASL_OK; +} + +void BongoSaslShutdown(void) { sasl_server_done(); } + +int +BongoSaslSessionCreate(BongoSaslSession **output, const char *service, + const char *hostname, const char *local_address, + const char *remote_address, int tls_active, + BongoSaslVerify verify, void *context) +{ + BongoSaslSession *session; + sasl_server_userdb_checkpass_t *typed_callback = check_password; + sasl_ssf_t external_ssf = tls_active ? 256 : 0; + if (!output || !service || !verify) return 0; + *output = NULL; + session = calloc(1, sizeof(*session)); + if (!session) return 0; + session->verify = verify; + session->context = context; + session->callbacks[0].id = SASL_CB_SERVER_USERDB_CHECKPASS; + _Static_assert(sizeof(session->callbacks[0].proc) == sizeof(typed_callback), + "Cyrus SASL callback pointer size mismatch"); + memcpy(&session->callbacks[0].proc, &typed_callback, + sizeof(session->callbacks[0].proc)); + session->callbacks[0].context = session; + session->callbacks[1].id = SASL_CB_LIST_END; + if (sasl_server_new(service, hostname, NULL, local_address, remote_address, + session->callbacks, 0, &session->connection) != SASL_OK || + sasl_setprop(session->connection, SASL_SSF_EXTERNAL, &external_ssf) != SASL_OK) { + BongoSaslSessionDestroy(&session); + return 0; + } + *output = session; + return 1; +} + +void +BongoSaslSessionDestroy(BongoSaslSession **session) +{ + if (!session || !*session) return; + if ((*session)->connection) sasl_dispose(&(*session)->connection); + free(*session); + *session = NULL; +} + +int +BongoSaslMechanisms(BongoSaslSession *session, const char **mechanisms) +{ + const char *available; + char *copy, *token, *save = NULL; + size_t used = 0; + unsigned length; + int count; + if (!session || !mechanisms) return 0; + if (sasl_listmech(session->connection, NULL, "", " ", "", &available, + &length, &count) != SASL_OK || !length || count <= 0 || + !(copy = strdup(available))) return 0; + session->mechanisms[0] = '\0'; + for (token = strtok_r(copy, " ", &save); token; token = strtok_r(NULL, " ", &save)) { + size_t token_length = strlen(token); + /* The current Bongo callback can verify password mechanisms only. + * SCRAM/OAUTHBEARER/EXTERNAL are advertised once their credential + * callbacks and app-password policy are wired end to end. */ + if (strcmp(token, "PLAIN") && strcmp(token, "LOGIN")) continue; + if (used + token_length + (used ? 1 : 0) >= sizeof(session->mechanisms)) break; + if (used) session->mechanisms[used++] = ' '; + memcpy(session->mechanisms + used, token, token_length + 1); + used += token_length; + } + free(copy); + *mechanisms = session->mechanisms; + return used != 0; +} + +static int +exchange_result(int result, const char *sasl_output, unsigned sasl_length, + const void **output, size_t *output_length) +{ + if (output) *output = sasl_output; + if (output_length) *output_length = sasl_length; + if (result == SASL_OK) return BONGO_SASL_COMPLETE; + if (result == SASL_CONTINUE) return BONGO_SASL_CONTINUE; + return BONGO_SASL_FAIL; +} + +int +BongoSaslStart(BongoSaslSession *session, const char *mechanism, + const void *input, size_t input_length, + const void **output, size_t *output_length) +{ + const char *sasl_output = NULL; + unsigned sasl_length = 0; + int result; + if (!session || !mechanism || input_length > (size_t)UINT_MAX) return BONGO_SASL_FAIL; + result = sasl_server_start(session->connection, mechanism, input, + (unsigned)input_length, &sasl_output, &sasl_length); + return exchange_result(result, sasl_output, sasl_length, output, output_length); +} + +int +BongoSaslStep(BongoSaslSession *session, const void *input, size_t input_length, + const void **output, size_t *output_length) +{ + const char *sasl_output = NULL; + unsigned sasl_length = 0; + int result; + if (!session || input_length > (size_t)UINT_MAX) return BONGO_SASL_FAIL; + result = sasl_server_step(session->connection, input, (unsigned)input_length, + &sasl_output, &sasl_length); + return exchange_result(result, sasl_output, sasl_length, output, output_length); +} + +const char * +BongoSaslUsername(BongoSaslSession *session) +{ + const void *username = NULL; + if (!session || sasl_getprop(session->connection, SASL_USERNAME, &username) != SASL_OK) + return NULL; + return username; +} + +int +BongoSaslDecode64(const char *input, size_t input_length, void *output, + size_t output_capacity, size_t *output_length) +{ + unsigned decoded = 0; + if (!input || !output || !output_length || input_length > UINT_MAX || + output_capacity > UINT_MAX) return 0; + if (sasl_decode64(input, (unsigned)input_length, output, + (unsigned)output_capacity, &decoded) != SASL_OK) return 0; + *output_length = decoded; + return 1; +} + +int +BongoSaslEncode64(const void *input, size_t input_length, char *output, + size_t output_capacity, size_t *output_length) +{ + unsigned encoded = 0; + if (!input || !output || !output_length || input_length > UINT_MAX || + output_capacity > UINT_MAX) return 0; + if (sasl_encode64(input, (unsigned)input_length, output, + (unsigned)output_capacity, &encoded) != SASL_OK) return 0; + *output_length = encoded; + return 1; +} diff --git a/src/libs/sasl/tests/sasl-test.c b/src/libs/sasl/tests/sasl-test.c new file mode 100644 index 0000000..d47305e --- /dev/null +++ b/src/libs/sasl/tests/sasl-test.c @@ -0,0 +1,43 @@ +#include +#include + +static int verify(void *context, const char *user, const char *password, + size_t password_length) +{ + (void)context; + return !strcmp(user, "alice@example.test") && password_length == 6 && + !memcmp(password, "secret", 6); +} + +int main(void) +{ + static const char plain[] = "\0alice@example.test\0secret"; + BongoSaslSession *session = NULL; + const char *mechanisms = NULL; + const void *output = NULL; + size_t output_length = 0; + char encoded[128]; + char decoded[128]; + size_t encoded_length; + size_t decoded_length; + int result = 1; + if (!BongoSaslInitialize()) return 1; + if (!BongoSaslSessionCreate(&session, "sieve", "mail.example.test", + "127.0.0.1;4190", "127.0.0.1;12345", 1, + verify, NULL)) goto finish; + if (!BongoSaslMechanisms(session, &mechanisms) || !strstr(mechanisms, "PLAIN")) goto finish; + if (BongoSaslStart(session, "PLAIN", plain, sizeof(plain) - 1, + &output, &output_length) != BONGO_SASL_COMPLETE) goto finish; + if (!BongoSaslUsername(session) || strcmp(BongoSaslUsername(session), + "alice@example.test")) goto finish; + if (!BongoSaslEncode64(plain, sizeof(plain) - 1, encoded, sizeof(encoded), + &encoded_length) || + !BongoSaslDecode64(encoded, encoded_length, decoded, sizeof(decoded), + &decoded_length) || decoded_length != sizeof(plain) - 1 || + memcmp(decoded, plain, decoded_length)) goto finish; + result = 0; +finish: + BongoSaslSessionDestroy(&session); + BongoSaslShutdown(); + return result; +} diff --git a/src/libs/sieve/CMakeLists.txt b/src/libs/sieve/CMakeLists.txt new file mode 100644 index 0000000..366b179 --- /dev/null +++ b/src/libs/sieve/CMakeLists.txt @@ -0,0 +1,14 @@ +add_library(libbongosieve sieve.c store.c managesieve.c) +set_target_properties(libbongosieve PROPERTIES OUTPUT_NAME bongosieve) +target_include_directories(libbongosieve PRIVATE ${MAILUTILS_INCLUDE_DIR}) +target_include_directories(libbongosieve PRIVATE ${SQLITE_INCLUDE_DIRS}) +target_link_libraries(libbongosieve PRIVATE ${MAILUTILS_LIBRARIES} ${SQLITE_LIBRARIES}) + +install(TARGETS libbongosieve DESTINATION ${LIB_INSTALL_DIR}) + +if(BUILD_TESTING) + add_executable(bongo-sieve-test tests/sieve-test.c tests/store-test.c + tests/managesieve-test.c) + target_link_libraries(bongo-sieve-test PRIVATE libbongosieve) + add_test(NAME sieve-validation COMMAND bongo-sieve-test) +endif() diff --git a/src/libs/sieve/managesieve.c b/src/libs/sieve/managesieve.c new file mode 100644 index 0000000..577e995 --- /dev/null +++ b/src/libs/sieve/managesieve.c @@ -0,0 +1,134 @@ +#include +#include +#include +#include +#include + +struct command_name { const char *name; BongoManageSieveCommand command; }; +static const struct command_name commands[] = { + {"AUTHENTICATE", BONGO_MANAGESIEVE_AUTHENTICATE}, + {"CAPABILITY", BONGO_MANAGESIEVE_CAPABILITY}, + {"CHECKSCRIPT", BONGO_MANAGESIEVE_CHECKSCRIPT}, + {"DELETESCRIPT", BONGO_MANAGESIEVE_DELETESCRIPT}, + {"GETSCRIPT", BONGO_MANAGESIEVE_GETSCRIPT}, + {"HAVESPACE", BONGO_MANAGESIEVE_HAVESPACE}, + {"LISTSCRIPTS", BONGO_MANAGESIEVE_LISTSCRIPTS}, + {"LOGOUT", BONGO_MANAGESIEVE_LOGOUT}, + {"NOOP", BONGO_MANAGESIEVE_NOOP}, + {"PUTSCRIPT", BONGO_MANAGESIEVE_PUTSCRIPT}, + {"RENAMESCRIPT", BONGO_MANAGESIEVE_RENAMESCRIPT}, + {"SETACTIVE", BONGO_MANAGESIEVE_SETACTIVE}, + {"STARTTLS", BONGO_MANAGESIEVE_STARTTLS} +}; + +static int +token(const char **cursor, const char *end, char *output, size_t capacity, + size_t *literal_length, int *nonsynchronizing) +{ + size_t used = 0; + while (*cursor < end && (**cursor == ' ' || **cursor == '\t')) (*cursor)++; + if (*cursor >= end) return 0; + if (**cursor == '"') { + (*cursor)++; + while (*cursor < end && **cursor != '"') { + char value = *(*cursor)++; + if (value == '\\') { + if (*cursor >= end || (**cursor != '\\' && **cursor != '"')) return -1; + value = *(*cursor)++; + } + if ((unsigned char)value < 0x20 || used + 1 >= capacity) return -1; + output[used++] = value; + } + if (*cursor >= end || *(*cursor)++ != '"') return -1; + } else if (**cursor == '{') { + char *number_end; + unsigned long number; + const char *start = ++*cursor; + if (start >= end || !isdigit((unsigned char)*start)) return -1; + number = strtoul(start, &number_end, 10); + if (number_end == start || number > 1024 * 1024 || number_end >= end) return -1; + *cursor = number_end; + if (**cursor == '+') { *nonsynchronizing = 1; (*cursor)++; } + if (*cursor >= end || *(*cursor)++ != '}') return -1; + *literal_length = (size_t)number; + output[0] = '\0'; + return 2; + } else { + while (*cursor < end && **cursor != ' ' && **cursor != '\t') { + unsigned char value = (unsigned char)*(*cursor)++; + if (value < 0x21 || value > 0x7e || value == '{' || value == '}' || + value == '"' || value == '\\' || used + 1 >= capacity) return -1; + output[used++] = (char)value; + } + } + output[used] = '\0'; + return 1; +} + +int +BongoManageSieveParse(const char *line, size_t length, BongoManageSieveRequest *request) +{ + const char *cursor; + const char *end; + char command[32]; + size_t index; + int result; + if (!line || !request || !length || length > 8192) return 0; + memset(request, 0, sizeof(*request)); + while (length && (line[length - 1] == '\r' || line[length - 1] == '\n')) length--; + cursor = line; + end = line + length; + result = token(&cursor, end, command, sizeof(command), &request->literal_length, + &request->literal_nonsynchronizing); + if (result != 1) return 0; + for (index = 0; index < sizeof(commands) / sizeof(commands[0]); index++) { + if (!strcasecmp(command, commands[index].name)) { + request->command = commands[index].command; + break; + } + } + if (request->command == BONGO_MANAGESIEVE_INVALID) return 0; + while (cursor < end) { + while (cursor < end && (*cursor == ' ' || *cursor == '\t')) cursor++; + if (cursor == end) break; + if (request->argument_count >= 3) return 0; + result = token(&cursor, end, request->arguments[request->argument_count], + sizeof(request->arguments[0]), &request->literal_length, + &request->literal_nonsynchronizing); + if (result < 1) return 0; + request->argument_count++; + if (result == 2 && cursor != end) return 0; + } + switch (request->command) { + case BONGO_MANAGESIEVE_CAPABILITY: + case BONGO_MANAGESIEVE_LISTSCRIPTS: + case BONGO_MANAGESIEVE_LOGOUT: + case BONGO_MANAGESIEVE_STARTTLS: + return request->argument_count == 0; + case BONGO_MANAGESIEVE_NOOP: + return request->argument_count <= 1 && request->literal_length == 0; + case BONGO_MANAGESIEVE_GETSCRIPT: + case BONGO_MANAGESIEVE_DELETESCRIPT: + case BONGO_MANAGESIEVE_SETACTIVE: + return request->argument_count == 1 && request->literal_length == 0; + case BONGO_MANAGESIEVE_RENAMESCRIPT: + return request->argument_count == 2 && request->literal_length == 0; + case BONGO_MANAGESIEVE_HAVESPACE: { + char *number_end; + unsigned long amount; + if (request->argument_count != 2 || request->literal_length) return 0; + amount = strtoul(request->arguments[1], &number_end, 10); + return *request->arguments[1] && !*number_end && amount <= 1024 * 1024; + } + case BONGO_MANAGESIEVE_PUTSCRIPT: + return request->argument_count == 2 && request->literal_length > 0; + case BONGO_MANAGESIEVE_CHECKSCRIPT: + return request->argument_count == 1 && request->literal_length > 0; + case BONGO_MANAGESIEVE_AUTHENTICATE: + return request->argument_count >= 1 && request->argument_count <= 2 && + request->literal_length == 0; + case BONGO_MANAGESIEVE_INVALID: + default: + return 0; + } +} diff --git a/src/libs/sieve/sieve.c b/src/libs/sieve/sieve.c new file mode 100644 index 0000000..168e579 --- /dev/null +++ b/src/libs/sieve/sieve.c @@ -0,0 +1,307 @@ +#include +#include +#include +#include +#include + +static int install_bongo_actions(mu_sieve_machine_t machine); + +static int +safe_requirement(const char *name, size_t length) +{ + static const char *allowed[] = { + "fileinto", "redirect", "reject", "envelope", "body", "variables", + "vacation", "vacation-seconds" + }; + size_t index; + for (index = 0; index < sizeof(allowed) / sizeof(allowed[0]); index++) { + if (strlen(allowed[index]) == length && + !memcmp(name, allowed[index], length)) return 1; + } + return 0; +} + +/* Mailutils supports loadable Sieve modules, including pipe/include-style + * facilities which must never be available to untrusted mailbox users. */ +static int +safe_requirements(const char *script, size_t length) +{ + size_t cursor = 0; + int in_require = 0; + int has_days = 0, has_seconds = 0; + while (cursor < length) { + if (script[cursor] == '#') { + while (cursor < length && script[cursor] != '\n') cursor++; + } else if (cursor + 1 < length && script[cursor] == '/' && script[cursor + 1] == '*') { + cursor += 2; + while (cursor + 1 < length && !(script[cursor] == '*' && script[cursor + 1] == '/')) cursor++; + if (cursor + 1 >= length) return 0; + cursor += 2; + } else if (script[cursor] == '"') { + size_t start = ++cursor; + while (cursor < length && script[cursor] != '"') { + if (script[cursor] == '\\' && ++cursor >= length) return 0; + cursor++; + } + if (cursor >= length) return 0; + if (in_require && !safe_requirement(script + start, cursor - start)) return 0; + cursor++; + } else if (script[cursor] == ':' && cursor + 5 < length) { + cursor++; + if (cursor + 4 <= length && !strncasecmp(script + cursor, "days", 4) && + (cursor + 4 == length || !isalnum((unsigned char)script[cursor + 4]))) { + has_days = 1; + } else if (cursor + 7 <= length && !strncasecmp(script + cursor, "seconds", 7) && + (cursor + 7 == length || !isalnum((unsigned char)script[cursor + 7]))) { + has_seconds = 1; + } + } else if ((script[cursor] >= 'A' && script[cursor] <= 'Z') || + (script[cursor] >= 'a' && script[cursor] <= 'z') || script[cursor] == '_') { + size_t start = cursor++; + while (cursor < length && ((script[cursor] >= 'A' && script[cursor] <= 'Z') || + (script[cursor] >= 'a' && script[cursor] <= 'z') || + (script[cursor] >= '0' && script[cursor] <= '9') || script[cursor] == '_')) cursor++; + if (cursor - start == 7 && !strncasecmp(script + start, "require", 7)) in_require = 1; + } else { + if (in_require && script[cursor] == ';') in_require = 0; + cursor++; + } + } + return !in_require && !(has_days && has_seconds); +} + +static char * +normalize_script(const char *script, size_t length, size_t *normalized_length) +{ + char *normalized; + size_t input, output = 0; + if (!script || !length || length > 1024 * 1024 || + !safe_requirements(script, length)) return NULL; + normalized = malloc(length + 1); + if (!normalized) return NULL; + for (input = 0; input < length; input++) { + if (script[input] == '\0') { free(normalized); return NULL; } + if (script[input] == '\r') { + if (input + 1 >= length || script[input + 1] != '\n') { + free(normalized); + return NULL; + } + continue; + } + normalized[output++] = script[input]; + } + normalized[output] = '\0'; + *normalized_length = output; + return normalized; +} + +int +BongoSieveValidate(const char *script, size_t length) +{ + mu_sieve_machine_t machine = NULL; + struct mu_locus_point origin = MU_LOCUS_POINT_INITIALIZER; + char *normalized; + size_t output = 0; + int result; + + normalized = normalize_script(script, length, &output); + if (!normalized) return 0; + if (mu_sieve_machine_create(&machine) != 0 || !machine) { + free(normalized); + return 0; + } + if (!install_bongo_actions(machine)) { + mu_sieve_machine_destroy(&machine); + free(normalized); + return 0; + } + mu_sieve_set_dry_run(machine, 1); + origin.mu_file = "bongo-upload.sieve"; + origin.mu_line = 1; + origin.mu_col = 1; + result = mu_sieve_compile_text(machine, normalized, output, &origin); + mu_sieve_machine_destroy(&machine); + free(normalized); + return result == 0; +} + +struct evaluation_context { + BongoSieveActionCallback callback; + void *data; + int valid; + BongoSieveResult actions[256]; + size_t action_count; +}; + +static int +queue_action(struct evaluation_context *context, const BongoSieveResult *result) +{ + if (!context || !context->valid || context->action_count >= 256) return 0; + context->actions[context->action_count++] = *result; + return 1; +} + +static int +report_action(mu_sieve_machine_t machine, BongoSieveAction action, + int has_argument) +{ + struct evaluation_context *context = mu_sieve_get_data(machine); + BongoSieveResult result = { .action = action, .days = 7 }; + if (!context || !context->valid) return 1; + if (has_argument) mu_sieve_get_arg(machine, 0, SVT_STRING, &result.argument); + if ((has_argument && (!result.argument || !*result.argument)) || + !queue_action(context, &result)) { + context->valid = 0; + return 1; + } + return 0; +} + +static int action_keep(mu_sieve_machine_t m) { return report_action(m, BONGO_SIEVE_KEEP, 0); } +static int action_discard(mu_sieve_machine_t m) { return report_action(m, BONGO_SIEVE_DISCARD, 0); } +static int action_fileinto(mu_sieve_machine_t m) { return report_action(m, BONGO_SIEVE_FILEINTO, 1); } +static int action_redirect(mu_sieve_machine_t m) { return report_action(m, BONGO_SIEVE_REDIRECT, 1); } +static int action_reject(mu_sieve_machine_t m) { return report_action(m, BONGO_SIEVE_REJECT, 1); } + +static int +vacation_tag_checker(mu_sieve_machine_t machine) +{ + size_t arguments = 0, tags = 0, index; + int days = 0, seconds = 0; + mu_sieve_get_argc(machine, &arguments, &tags); + (void)arguments; + for (index = 0; index < tags; index++) { + mu_sieve_value_t *tag = mu_sieve_get_tag_n(machine, index); + if (tag && tag->tag) { + if (!strcmp(tag->tag, "days")) days = 1; + else if (!strcmp(tag->tag, "seconds")) seconds = 1; + } + } + return days && seconds; +} + +static int +action_vacation(mu_sieve_machine_t machine) +{ + struct evaluation_context *context = mu_sieve_get_data(machine); + BongoSieveResult result = { .action = BONGO_SIEVE_VACATION, .days = 7 }; + size_t days = 7; + size_t seconds = 0; + mu_sieve_value_t *addresses; + size_t index; + if (!context || !context->valid) return 1; + mu_sieve_get_arg(machine, 0, SVT_STRING, &result.argument); + if (mu_sieve_get_tag(machine, "days", SVT_NUMBER, &days)) result.days = (unsigned long)days; + mu_sieve_get_tag(machine, "subject", SVT_STRING, &result.subject); + mu_sieve_get_tag(machine, "from", SVT_STRING, &result.from); + mu_sieve_get_tag(machine, "handle", SVT_STRING, &result.handle); + addresses = mu_sieve_get_tag_untyped(machine, "addresses"); + if (addresses && addresses->type == SVT_STRING_LIST) { + result.address_count = addresses->v.list.count; + if (result.address_count > 32) result.address_count = 32; + for (index = 0; index < result.address_count; index++) { + result.addresses[index] = mu_sieve_string(machine, &addresses->v.list, index); + } + } + if (mu_sieve_get_tag(machine, "seconds", SVT_NUMBER, &seconds)) { + result.seconds = (unsigned long)seconds; + result.has_seconds = 1; + } + result.mime = mu_sieve_get_tag(machine, "mime", SVT_VOID, NULL) != 0; + if (!result.argument || !*result.argument || result.days > 365 || + result.seconds > 366UL * 86400UL || + (result.has_seconds && mu_sieve_get_tag_untyped(machine, "days")) || + !queue_action(context, &result)) { + context->valid = 0; + return 1; + } + return 0; +} + +static int +install_bongo_actions(mu_sieve_machine_t machine) +{ + static mu_sieve_data_type string_argument[] = { SVT_STRING, SVT_VOID }; + static mu_sieve_data_type no_arguments[] = { SVT_VOID }; + static mu_sieve_tag_def_t vacation_tags[] = { + { "days", SVT_NUMBER }, { "subject", SVT_STRING }, + { "from", SVT_STRING }, { "addresses", SVT_STRING_LIST }, + { "mime", SVT_VOID }, { "handle", SVT_STRING }, + { "seconds", SVT_NUMBER }, { NULL, SVT_VOID } + }; + static mu_sieve_tag_group_t vacation_groups[] = { + { vacation_tags, vacation_tag_checker }, { NULL, NULL } + }; + mu_sieve_register_action(machine, "keep", action_keep, + no_arguments, NULL, 1); + mu_sieve_register_action(machine, "discard", action_discard, + no_arguments, NULL, 1); + mu_sieve_register_action(machine, "fileinto", action_fileinto, + string_argument, NULL, 0); + mu_sieve_register_action(machine, "redirect", action_redirect, + string_argument, NULL, 0); + mu_sieve_register_action(machine, "reject", action_reject, + string_argument, NULL, 0); + mu_sieve_register_action(machine, "vacation", action_vacation, + string_argument, vacation_groups, 1); + /* RFC 6131 uses this name only as a require/capability identifier. */ + mu_sieve_register_action(machine, "vacation-seconds", action_vacation, + string_argument, vacation_groups, 0); + return mu_sieve_registry_lookup(machine, "fileinto", mu_sieve_record_action) && + mu_sieve_registry_lookup(machine, "redirect", mu_sieve_record_action) && + mu_sieve_registry_lookup(machine, "reject", mu_sieve_record_action) && + mu_sieve_registry_lookup(machine, "vacation", mu_sieve_record_action) && + mu_sieve_registry_lookup(machine, "vacation-seconds", mu_sieve_record_action); +} + +int +BongoSieveEvaluate(const char *script, size_t script_length, + const char *message, size_t message_length, + BongoSieveActionCallback callback, void *data) +{ + mu_sieve_machine_t machine = NULL; + mu_message_t mail = NULL; + mu_stream_t stream = NULL; + struct mu_locus_point origin = MU_LOCUS_POINT_INITIALIZER; + struct evaluation_context context = { .callback = callback, .data = data, .valid = 1 }; + char *normalized; + size_t normalized_length = 0; + int result = 0; + if (!message || !message_length || !callback) return 0; + normalized = normalize_script(script, script_length, &normalized_length); + if (!normalized) return 0; + origin.mu_file = "bongo-active.sieve"; + origin.mu_line = 1; + origin.mu_col = 1; + if (mu_sieve_machine_create(&machine) != 0 || !machine || + mu_static_memory_stream_create(&stream, message, message_length) != 0 || + mu_message_create(&mail, NULL) != 0 || + mu_message_set_stream(mail, stream, NULL) != 0) goto finish; + mu_sieve_set_data(machine, &context); + if (!install_bongo_actions(machine) || + mu_sieve_compile_text(machine, normalized, normalized_length, &origin) != 0) goto finish; + if (mu_sieve_message(machine, mail) == 0 && context.valid) { + size_t index; + int reject_count = 0, has_delivery = 0, has_vacation = 0; + for (index = 0; index < context.action_count; index++) { + BongoSieveAction action = context.actions[index].action; + if (action == BONGO_SIEVE_REJECT) reject_count++; + if (action == BONGO_SIEVE_VACATION) has_vacation = 1; + if (action == BONGO_SIEVE_KEEP || action == BONGO_SIEVE_FILEINTO || + action == BONGO_SIEVE_REDIRECT) has_delivery = 1; + } + if (reject_count > 1 || (reject_count && (has_delivery || has_vacation))) { + context.valid = 0; + } + for (index = 0; context.valid && index < context.action_count; index++) { + if (!callback(&context.actions[index], data)) context.valid = 0; + } + if (context.valid) result = 1; + } +finish: + if (mail) mu_message_destroy(&mail, NULL); + if (stream) mu_stream_unref(stream); + if (machine) mu_sieve_machine_destroy(&machine); + free(normalized); + return result; +} diff --git a/src/libs/sieve/store.c b/src/libs/sieve/store.c new file mode 100644 index 0000000..b2ce1c1 --- /dev/null +++ b/src/libs/sieve/store.c @@ -0,0 +1,260 @@ +#include +#include +#include +#include + +struct BongoSieveStore { + sqlite3 *database; +}; + +static int +valid_identity(const char *value, size_t maximum, int script_name) +{ + size_t length; + const unsigned char *cursor; + if (!value || !(length = strlen(value)) || length > maximum) return 0; + for (cursor = (const unsigned char *)value; *cursor; cursor++) { + if (*cursor < 0x20 || *cursor == 0x7f || (script_name && (*cursor == '/' || *cursor == '\\'))) { + return 0; + } + } + return 1; +} + +int +BongoSieveStoreOpen(BongoSieveStore **output, const char *path) +{ + BongoSieveStore *store; + static const char schema[] = + "PRAGMA journal_mode=WAL;" + "PRAGMA foreign_keys=ON;" + "CREATE TABLE IF NOT EXISTS sieve_scripts (" + " user TEXT NOT NULL, name TEXT NOT NULL, script BLOB NOT NULL," + " active INTEGER NOT NULL DEFAULT 0 CHECK(active IN (0,1))," + " revision INTEGER NOT NULL DEFAULT 1, updated_at INTEGER NOT NULL," + " PRIMARY KEY(user,name));" + "CREATE UNIQUE INDEX IF NOT EXISTS sieve_one_active " + " ON sieve_scripts(user) WHERE active=1;" + "CREATE TABLE IF NOT EXISTS sieve_vacation_replies (" + " user TEXT NOT NULL, sender TEXT NOT NULL, handle TEXT NOT NULL," + " replied_at INTEGER NOT NULL, PRIMARY KEY(user,sender,handle));"; + if (!output || !path || !*path) return 0; + *output = NULL; + store = calloc(1, sizeof(*store)); + if (!store) return 0; + if (sqlite3_open_v2(path, &store->database, + SQLITE_OPEN_READWRITE | SQLITE_OPEN_CREATE, NULL) != SQLITE_OK || + sqlite3_busy_timeout(store->database, 5000) != SQLITE_OK || + sqlite3_exec(store->database, schema, NULL, NULL, NULL) != SQLITE_OK) { + BongoSieveStoreClose(&store); + return 0; + } + *output = store; + return 1; +} + +void +BongoSieveStoreClose(BongoSieveStore **store) +{ + if (!store || !*store) return; + if ((*store)->database) sqlite3_close((*store)->database); + free(*store); + *store = NULL; +} + +int +BongoSieveStorePut(BongoSieveStore *store, const char *user, + const char *name, const char *script, size_t length) +{ + sqlite3_stmt *statement = NULL; + int result = 0; + static const char sql[] = + "INSERT INTO sieve_scripts(user,name,script,updated_at) " + "VALUES(?,?,?,unixepoch()) ON CONFLICT(user,name) DO UPDATE SET " + "script=excluded.script, revision=revision+1, updated_at=unixepoch()"; + if (!store || !valid_identity(user, 320, 0) || + !valid_identity(name, 128, 1) || !BongoSieveValidate(script, length)) return 0; + if (sqlite3_prepare_v2(store->database, sql, -1, &statement, NULL) == SQLITE_OK && + sqlite3_bind_text(statement, 1, user, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_text(statement, 2, name, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_blob(statement, 3, script, (int)length, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_step(statement) == SQLITE_DONE) result = 1; + sqlite3_finalize(statement); + return result; +} + +int +BongoSieveStoreGet(BongoSieveStore *store, const char *user, + const char *name, char **script, size_t *length, int *active) +{ + sqlite3_stmt *statement = NULL; + const void *data; + int bytes; + int result = 0; + const char *sql = name ? + "SELECT script,active FROM sieve_scripts WHERE user=? AND name=?" : + "SELECT script,active FROM sieve_scripts WHERE user=? AND active=1"; + if (!store || !script || !length || !valid_identity(user, 320, 0) || + (name && !valid_identity(name, 128, 1))) return 0; + *script = NULL; + *length = 0; + if (sqlite3_prepare_v2(store->database, sql, -1, &statement, NULL) != SQLITE_OK || + sqlite3_bind_text(statement, 1, user, -1, SQLITE_TRANSIENT) != SQLITE_OK || + (name && sqlite3_bind_text(statement, 2, name, -1, SQLITE_TRANSIENT) != SQLITE_OK) || + sqlite3_step(statement) != SQLITE_ROW) goto finish; + data = sqlite3_column_blob(statement, 0); + bytes = sqlite3_column_bytes(statement, 0); + *script = malloc((size_t)bytes + 1); + if (!*script) goto finish; + memcpy(*script, data, (size_t)bytes); + (*script)[bytes] = '\0'; + *length = (size_t)bytes; + if (active) *active = sqlite3_column_int(statement, 1); + result = 1; +finish: + sqlite3_finalize(statement); + return result; +} + +int +BongoSieveStoreSetActive(BongoSieveStore *store, const char *user, const char *name) +{ + sqlite3_stmt *statement = NULL; + int result = 0; + if (!store || !valid_identity(user, 320, 0) || + (name && *name && !valid_identity(name, 128, 1))) return 0; + if (sqlite3_exec(store->database, "BEGIN IMMEDIATE", NULL, NULL, NULL) != SQLITE_OK) return 0; + if (sqlite3_prepare_v2(store->database, + "UPDATE sieve_scripts SET active=0 WHERE user=?", -1, + &statement, NULL) == SQLITE_OK && + sqlite3_bind_text(statement, 1, user, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_step(statement) == SQLITE_DONE) result = 1; + sqlite3_finalize(statement); + statement = NULL; + if (result && name && *name) { + result = 0; + if (sqlite3_prepare_v2(store->database, + "UPDATE sieve_scripts SET active=1 WHERE user=? AND name=?", + -1, &statement, NULL) == SQLITE_OK && + sqlite3_bind_text(statement, 1, user, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_text(statement, 2, name, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_step(statement) == SQLITE_DONE && + sqlite3_changes(store->database) == 1) result = 1; + sqlite3_finalize(statement); + } + sqlite3_exec(store->database, result ? "COMMIT" : "ROLLBACK", NULL, NULL, NULL); + return result; +} + +int +BongoSieveStoreDelete(BongoSieveStore *store, const char *user, const char *name) +{ + sqlite3_stmt *statement = NULL; + int result = 0; + if (!store || !valid_identity(user, 320, 0) || !valid_identity(name, 128, 1)) return 0; + if (sqlite3_prepare_v2(store->database, + "DELETE FROM sieve_scripts WHERE user=? AND name=? AND active=0", + -1, &statement, NULL) == SQLITE_OK && + sqlite3_bind_text(statement, 1, user, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_text(statement, 2, name, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(store->database) == 1) result = 1; + sqlite3_finalize(statement); + return result; +} + +int +BongoSieveStoreRename(BongoSieveStore *store, const char *user, + const char *old_name, const char *new_name) +{ + sqlite3_stmt *statement = NULL; + int result = 0; + if (!store || !valid_identity(user, 320, 0) || + !valid_identity(old_name, 128, 1) || !valid_identity(new_name, 128, 1) || + strcmp(old_name, new_name) == 0) return 0; + if (sqlite3_prepare_v2(store->database, + "UPDATE sieve_scripts SET name=?, revision=revision+1, " + "updated_at=unixepoch() WHERE user=? AND name=? AND NOT EXISTS " + "(SELECT 1 FROM sieve_scripts WHERE user=? AND name=?)", + -1, &statement, NULL) == SQLITE_OK && + sqlite3_bind_text(statement, 1, new_name, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_text(statement, 2, user, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_text(statement, 3, old_name, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_text(statement, 4, user, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_text(statement, 5, new_name, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(store->database) == 1) { + result = 1; + } + sqlite3_finalize(statement); + return result; +} + +int +BongoSieveVacationClaim(BongoSieveStore *store, const char *user, + const char *sender, const char *handle, + unsigned long interval_seconds) +{ + sqlite3_stmt *statement = NULL; + int result = 0; + static const char sql[] = + "INSERT INTO sieve_vacation_replies(user,sender,handle,replied_at) " + "VALUES(?,?,?,unixepoch()) ON CONFLICT(user,sender,handle) DO UPDATE SET " + "replied_at=unixepoch() WHERE replied_at <= unixepoch()-?"; + if (!store || !valid_identity(user, 320, 0) || + !valid_identity(sender, 320, 0) || !valid_identity(handle, 128, 1) || + interval_seconds > 366UL * 86400UL) return 0; + if (sqlite3_prepare_v2(store->database, sql, -1, &statement, NULL) == SQLITE_OK && + sqlite3_bind_text(statement, 1, user, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_text(statement, 2, sender, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_text(statement, 3, handle, -1, SQLITE_TRANSIENT) == SQLITE_OK && + sqlite3_bind_int64(statement, 4, (sqlite3_int64)interval_seconds) == SQLITE_OK && + sqlite3_step(statement) == SQLITE_DONE && sqlite3_changes(store->database) == 1) { + result = 1; + } + sqlite3_finalize(statement); + return result; +} + +void +BongoSieveVacationRelease(BongoSieveStore *store, const char *user, + const char *sender, const char *handle) +{ + sqlite3_stmt *statement = NULL; + if (!store || !user || !sender || !handle) return; + if (sqlite3_prepare_v2(store->database, + "DELETE FROM sieve_vacation_replies WHERE user=? AND sender=? AND handle=?", + -1, &statement, NULL) == SQLITE_OK) { + sqlite3_bind_text(statement, 1, user, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(statement, 2, sender, -1, SQLITE_TRANSIENT); + sqlite3_bind_text(statement, 3, handle, -1, SQLITE_TRANSIENT); + sqlite3_step(statement); + } + sqlite3_finalize(statement); +} + +int +BongoSieveStoreList(BongoSieveStore *store, const char *user, + BongoSieveListCallback callback, void *data) +{ + sqlite3_stmt *statement = NULL; + int result = 0; + if (!store || !callback || !valid_identity(user, 320, 0)) return 0; + if (sqlite3_prepare_v2(store->database, + "SELECT name,active FROM sieve_scripts WHERE user=? ORDER BY name", + -1, &statement, NULL) != SQLITE_OK || + sqlite3_bind_text(statement, 1, user, -1, SQLITE_TRANSIENT) != SQLITE_OK) { + sqlite3_finalize(statement); + return 0; + } + for (;;) { + int step = sqlite3_step(statement); + if (step == SQLITE_DONE) { + result = 1; + break; + } + if (step != SQLITE_ROW || !callback( + (const char *)sqlite3_column_text(statement, 0), + sqlite3_column_int(statement, 1), data)) break; + } + sqlite3_finalize(statement); + return result; +} diff --git a/src/libs/sieve/tests/managesieve-test.c b/src/libs/sieve/tests/managesieve-test.c new file mode 100644 index 0000000..02534ee --- /dev/null +++ b/src/libs/sieve/tests/managesieve-test.c @@ -0,0 +1,27 @@ +#include +#include + +int BongoManageSieveProtocolTest(void) +{ + BongoManageSieveRequest request; + const char put[] = "PUTSCRIPT \"Family rules\" {123+}\r\n"; + const char bad[] = "PUTSCRIPT \"x\\ny\" {9999999}\r\n"; + const char capability_extra[] = "CAPABILITY extra\r\n"; + const char put_quoted[] = "PUTSCRIPT \"main\" \"not-a-literal\"\r\n"; + const char havespace[] = "HAVESPACE \"main\" 1048576\r\n"; + const char havespace_bad[] = "HAVESPACE \"main\" -1\r\n"; + if (!BongoManageSieveParse(put, strlen(put), &request) || + request.command != BONGO_MANAGESIEVE_PUTSCRIPT || + request.argument_count != 2 || strcmp(request.arguments[0], "Family rules") || + request.literal_length != 123 || !request.literal_nonsynchronizing) return 20; + if (BongoManageSieveParse(bad, strlen(bad), &request)) return 21; + if (!BongoManageSieveParse("CAPABILITY\r\n", 12, &request) || + request.command != BONGO_MANAGESIEVE_CAPABILITY || request.argument_count) return 22; + if (BongoManageSieveParse(capability_extra, strlen(capability_extra), + &request)) return 23; + if (BongoManageSieveParse(put_quoted, strlen(put_quoted), &request)) return 24; + if (!BongoManageSieveParse(havespace, strlen(havespace), &request)) return 25; + if (BongoManageSieveParse(havespace_bad, strlen(havespace_bad), + &request)) return 26; + return 0; +} diff --git a/src/libs/sieve/tests/sieve-test.c b/src/libs/sieve/tests/sieve-test.c new file mode 100644 index 0000000..f9c3242 --- /dev/null +++ b/src/libs/sieve/tests/sieve-test.c @@ -0,0 +1,128 @@ +#include +#include + +int BongoSieveStoreTest(void); +int BongoManageSieveProtocolTest(void); + +struct action_result { + BongoSieveAction action; + char argument[64]; + char subject[64]; + unsigned long days; + unsigned long seconds; + int has_seconds; + int count; +}; +static int capture_action(const BongoSieveResult *action, void *data) +{ + struct action_result *result = data; + result->action = action->action; + result->count++; + if (action->argument) { + strncpy(result->argument, action->argument, sizeof(result->argument) - 1); + result->argument[sizeof(result->argument) - 1] = '\0'; + } + if (action->subject) { + strncpy(result->subject, action->subject, sizeof(result->subject) - 1); + result->subject[sizeof(result->subject) - 1] = '\0'; + } + result->days = action->days; + result->seconds = action->seconds; + result->has_seconds = action->has_seconds; + return 1; +} + +int +main(void) +{ + static const char valid[] = + "require [\"fileinto\"];\n" + "if address :domain :is \"from\" \"family.example\" {\n" + " fileinto \"Family\";\n" + " stop;\n" + "}\n"; + static const char invalid[] = "if header { discard;\n"; + static const char crlf[] = "if true { keep; }\r\n"; + static const char bare_cr[] = "if true { keep; }\r"; + static const char dangerous[] = "require [\"pipe\"]; pipe \"/bin/true\";\n"; + + if (!BongoSieveValidate(valid, strlen(valid))) { + return 1; + } + if (BongoSieveValidate(invalid, strlen(invalid))) { + return 2; + } + if (BongoSieveValidate(valid, 1024 * 1024 + 1)) { + return 3; + } + if (!BongoSieveValidate(crlf, strlen(crlf))) { + return 4; + } + if (BongoSieveValidate(bare_cr, strlen(bare_cr))) { + return 5; + } + if (BongoSieveValidate(dangerous, strlen(dangerous))) return 7; + { + static const char message[] = + "From: sender@family.example\r\nTo: user@example.test\r\n" + "Subject: family note\r\n\r\nHello\r\n"; + struct action_result action = { 0 }; + if (!BongoSieveEvaluate(valid, strlen(valid), message, strlen(message), + capture_action, &action) || + action.count != 1 || action.action != BONGO_SIEVE_FILEINTO || + strcmp(action.argument, "Family")) return 6; + } + { + static const char vacation[] = + "require \"vacation\";\n" + "vacation :days 3 :subject \"Away\" \"Back soon\";\n"; + static const char message[] = + "From: friend@example.test\r\nTo: user@example.test\r\n\r\nHello\r\n"; + struct action_result action = { 0 }; + if (!BongoSieveValidate(vacation, strlen(vacation)) || + !BongoSieveEvaluate(vacation, strlen(vacation), message, strlen(message), + capture_action, &action) || + action.count != 1 || action.action != BONGO_SIEVE_VACATION || + action.days != 3 || strcmp(action.subject, "Away") || + strcmp(action.argument, "Back soon")) return 8; + } + { + static const char reject[] = + "require \"reject\"; reject \"Policy refusal\";\n"; + static const char double_reject[] = + "require \"reject\"; reject \"one\"; reject \"two\";\n"; + static const char reject_keep[] = + "require \"reject\"; reject \"no\"; keep;\n"; + static const char message[] = + "From: sender@example.test\r\nTo: user@example.test\r\n\r\nHello\r\n"; + struct action_result action = { 0 }; + if (!BongoSieveValidate(reject, strlen(reject)) || + !BongoSieveEvaluate(reject, strlen(reject), message, strlen(message), + capture_action, &action) || + action.count != 1 || action.action != BONGO_SIEVE_REJECT || + BongoSieveEvaluate(double_reject, strlen(double_reject), message, + strlen(message), capture_action, &action) || + BongoSieveEvaluate(reject_keep, strlen(reject_keep), message, + strlen(message), capture_action, &action)) return 10; + } + { + static const char vacation[] = + "require \"vacation-seconds\";\n" + "vacation :seconds 7200 \"Short break\";\n"; + static const char invalid[] = + "require \"vacation-seconds\"; vacation :days 1 :seconds 1 \"x\";\n"; + static const char message[] = + "From: friend@example.test\r\nTo: user@example.test\r\n\r\nHello\r\n"; + struct action_result action = { 0 }; + if (!BongoSieveValidate(vacation, strlen(vacation)) || + BongoSieveValidate(invalid, strlen(invalid)) || + !BongoSieveEvaluate(vacation, strlen(vacation), message, strlen(message), + capture_action, &action) || + action.action != BONGO_SIEVE_VACATION || !action.has_seconds || + action.seconds != 7200) return 9; + } + { + int result = BongoSieveStoreTest(); + return result ? result : BongoManageSieveProtocolTest(); + } +} diff --git a/src/libs/sieve/tests/store-test.c b/src/libs/sieve/tests/store-test.c new file mode 100644 index 0000000..4b2665a --- /dev/null +++ b/src/libs/sieve/tests/store-test.c @@ -0,0 +1,66 @@ +#include +#include +#include +#include + +static int store_test(void) +{ + char path[] = "/tmp/bongo-sieve-store-XXXXXX"; + const char script[] = "if true { keep; }\r\n"; + BongoSieveStore *store = NULL; + char *loaded = NULL; + size_t length = 0; + int active = 0; + int fd = mkstemp(path); + if (fd < 0) return 10; + close(fd); + if (!BongoSieveStoreOpen(&store, path) || + !BongoSieveStorePut(store, "alice@example.test", "main", script, strlen(script)) || + !BongoSieveStoreSetActive(store, "alice@example.test", "main") || + !BongoSieveStoreGet(store, "alice@example.test", NULL, &loaded, &length, &active) || + length != strlen(script) || memcmp(loaded, script, length) || !active || + BongoSieveStoreSetActive(store, "alice@example.test", "missing") || + BongoSieveStoreDelete(store, "alice@example.test", "main")) { + free(loaded); + BongoSieveStoreClose(&store); + unlink(path); + return 11; + } + free(loaded); + BongoSieveStoreSetActive(store, "alice@example.test", ""); + if (!BongoSieveStoreDelete(store, "alice@example.test", "main")) { + BongoSieveStoreClose(&store); + unlink(path); + return 12; + } + if (!BongoSieveStorePut(store, "alice@example.test", "old", script, strlen(script)) || + !BongoSieveStoreRename(store, "alice@example.test", "old", "renamed") || + BongoSieveStoreGet(store, "alice@example.test", "old", &loaded, &length, &active) || + !BongoSieveStoreGet(store, "alice@example.test", "renamed", &loaded, &length, &active)) { + free(loaded); + BongoSieveStoreClose(&store); + unlink(path); + return 13; + } + free(loaded); + if (!BongoSieveVacationClaim(store, "alice@example.test", "bob@example.test", + "holiday", 86400) || + BongoSieveVacationClaim(store, "alice@example.test", "bob@example.test", + "holiday", 86400)) { + BongoSieveStoreClose(&store); + unlink(path); + return 14; + } + BongoSieveVacationRelease(store, "alice@example.test", "bob@example.test", "holiday"); + if (!BongoSieveVacationClaim(store, "alice@example.test", "bob@example.test", + "holiday", 86400)) { + BongoSieveStoreClose(&store); + unlink(path); + return 15; + } + BongoSieveStoreClose(&store); + unlink(path); + return 0; +} + +int BongoSieveStoreTest(void) { return store_test(); } diff --git a/src/libs/streamio/bongostream.c b/src/libs/streamio/bongostream.c index 98c00fd..0d2f92d 100644 --- a/src/libs/streamio/bongostream.c +++ b/src/libs/streamio/bongostream.c @@ -52,6 +52,7 @@ Examine(const char *data, int size) static int StreamToMemory(StreamStruct *codec, StreamStruct *next) { + UNUSED_PARAMETER(next); #if DEBUG_BONGOSTREAM printf("appending %lu bytes\n", codec->Len); Examine(codec->Start, codec->Len); @@ -77,7 +78,7 @@ BongoStreamInsertCodec(BongoStream *stream, const char *name, BOOL encode, Strea printf("looking for %s\n", name); #endif - codec = FindCodec((char *)name, encode); + codec = FindCodec(name, encode); if (codec) { diff --git a/src/libs/streamio/codec/big5.h b/src/libs/streamio/codec/big5.h index a441692..d9a6c54 100644 --- a/src/libs/streamio/codec/big5.h +++ b/src/libs/streamio/codec/big5.h @@ -4188,7 +4188,7 @@ BIG5_Encode(StreamStruct *Codec, StreamStruct *NextCodec) UTF8DecodeChar(In, wc, Len, Codec->Len); - if (wc >= 0x0000 && wc < 0x0100) { + if (wc < 0x0100) { summary = &big5_uni2indx_page00[(wc>>4)]; } else if (wc >= 0x0200 && wc < 0x0460) { summary = &big5_uni2indx_page02[(wc>>4)-0x020]; diff --git a/src/libs/streamio/codec/euc_kr.h b/src/libs/streamio/codec/euc_kr.h index 27f97b9..97778e2 100644 --- a/src/libs/streamio/codec/euc_kr.h +++ b/src/libs/streamio/codec/euc_kr.h @@ -120,7 +120,7 @@ EUC_KR_Encode(StreamStruct *Codec, StreamStruct *NextCodec) UTF8DecodeChar(In, wc, Len, Codec->Len); - if (wc >= 0x0000 && wc < 0x0460) { + if (wc < 0x0460) { summary = &KSC5601_CP00[(wc>>4)]; } else if (wc >= 0x2000 && wc < 0x2670) { summary = &KSC5601_CP20[(wc>>4)-0x200]; diff --git a/src/libs/streamio/codec/euc_tw.h b/src/libs/streamio/codec/euc_tw.h index ee6a319..881b69c 100644 --- a/src/libs/streamio/codec/euc_tw.h +++ b/src/libs/streamio/codec/euc_tw.h @@ -179,7 +179,7 @@ EUC_TW_Encode(StreamStruct *Codec, StreamStruct *NextCodec) UTF8DecodeChar(In, wc, Len, Codec->Len); - if (wc >= 0x0000 && wc < 0x0100) { + if (wc < 0x0100) { summary = &cns11643_inv_uni2indx_page00[(wc>>4)]; } else if (wc >= 0x0200 && wc < 0x03d0) { summary = &cns11643_inv_uni2indx_page02[(wc>>4)-0x020]; diff --git a/src/libs/streamio/codec/gb2312.h b/src/libs/streamio/codec/gb2312.h index 7db04b2..f87bd0a 100644 --- a/src/libs/streamio/codec/gb2312.h +++ b/src/libs/streamio/codec/gb2312.h @@ -2601,7 +2601,7 @@ GB2312_Encode(StreamStruct *Codec, StreamStruct *NextCodec) UTF8DecodeChar(In, wc, Len, Codec->Len); - if (wc >= 0x0000 && wc < 0x0460) { + if (wc < 0x0460) { summary = &gb2312_uni2indx_page00[(wc>>4)]; } else if (wc >= 0x2000 && wc < 0x2650) { summary = &gb2312_uni2indx_page20[(wc>>4)-0x200]; diff --git a/src/libs/streamio/codec/html.h b/src/libs/streamio/codec/html.h index 3b5e2a3..8d38867 100644 --- a/src/libs/streamio/codec/html.h +++ b/src/libs/streamio/codec/html.h @@ -103,7 +103,7 @@ #define CODEC_HTML_SKIP_META_CHARSET_PRE_VALUE 42 #define CODEC_HTML_SKIP_META_CHARSET_VALUE 43 -unsigned char *StateString[] = { +static const char *const StateString[] = { "CODEC_HTML_SEND_CONTENT", "CODEC_HTML_SEND_TAG_EXAMINE", "CODEC_HTML_SEND_TAG", @@ -151,19 +151,19 @@ unsigned char *StateString[] = { }; typedef struct { - unsigned char *name; + const char *name; unsigned long state; } HtmlAttributeStruct; typedef struct { - unsigned char *name; + const char *name; unsigned long nameLen; unsigned long state; } HtmlAttributeInfoStruct; HtmlAttributeInfoStruct *HtmlAttributeInfo = NULL; -HtmlAttributeStruct HtmlAttribute[] = { +static const HtmlAttributeStruct HtmlAttribute[] = { {"href ", CODEC_HTML_SEND_TAG_URL_NAME}, {"href\t", CODEC_HTML_SEND_TAG_URL_NAME}, {"href=", CODEC_HTML_SEND_TAG_URL_NAME}, @@ -231,14 +231,14 @@ HtmlAttributeStruct HtmlAttribute[] = { }; typedef struct { - unsigned char *name; - unsigned char *endSupressTag; + const char *name; + const char *endSupressTag; unsigned long sendState; /* next state if the current mode is SEND */ unsigned long supressState; /* next state if the current mode is SUPPRESS */ unsigned long endSupressState; /* next state if the current mode is SUPPRESS and end tag is being processed */ } HtmlTagsStruct; -HtmlTagsStruct HtmlTag[] = { +static const HtmlTagsStruct HtmlTag[] = { {"", CODEC_HTML_SUPRESS_TAG, CODEC_HTML_SUPRESS_TAG, CODEC_HTML_SKIP_TAG}, @@ -264,7 +264,7 @@ HtmlTagsStruct HtmlTag[] = { }; typedef struct { - unsigned char *name; + const char *name; unsigned long nameLen; long endId; unsigned long sendState; @@ -402,6 +402,7 @@ HtmlDecodeFlushOutStream(unsigned long Space, unsigned char **Out, unsigned char __inline static void HtmlDecodeFlushNext(StreamStruct *Codec, StreamStruct *NextCodec, unsigned long Len, unsigned char *Out, FILE *debugHandle) { + UNUSED_PARAMETER(debugHandle); if (NextCodec->Len > 0) { if (Codec->EOS) { if (Len == Codec->Len) { @@ -436,6 +437,7 @@ HtmlDecodeFlushNext(StreamStruct *Codec, StreamStruct *NextCodec, unsigned long __inline static void CodecCopyChar(unsigned char **In, unsigned char **Out, unsigned long *Len, StreamStruct *NextCodec, FILE *debugHandle) { + UNUSED_PARAMETER(debugHandle); CopyCharDebugLog(debugHandle, In); (*Out)[0] = (*In)[0]; (*Out)++; @@ -447,6 +449,7 @@ CodecCopyChar(unsigned char **In, unsigned char **Out, unsigned long *Len, Strea __inline static void CodecSkipChar(unsigned char **In, unsigned long *Len, FILE *debugHandle) { + UNUSED_PARAMETER(debugHandle); SkipCharDebugLog(debugHandle, In); (*In)++; (*Len)++; @@ -455,6 +458,7 @@ CodecSkipChar(unsigned char **In, unsigned long *Len, FILE *debugHandle) __inline static void CodecInsertChar(unsigned char newChar, unsigned char **Out, StreamStruct *NextCodec, FILE *debugHandle) { + UNUSED_PARAMETER(debugHandle); InsertCharDebugLog(debugHandle, newChar); (*Out)[0] = newChar; (*Out)++; @@ -464,6 +468,7 @@ CodecInsertChar(unsigned char newChar, unsigned char **Out, StreamStruct *NextCo __inline static void CodecCopyChars(unsigned long size, unsigned char **In, unsigned char **Out, unsigned long *Len, StreamStruct *NextCodec, FILE *debugHandle) { + UNUSED_PARAMETER(debugHandle); CopyCharsDebugLog(debugHandle, size, In); memcpy(*Out, *In, size); (*Out) += size; @@ -475,14 +480,16 @@ CodecCopyChars(unsigned long size, unsigned char **In, unsigned char **Out, unsi __inline static void CodecSkipChars(unsigned long size, unsigned char **In, unsigned long *Len, FILE *debugHandle) { + UNUSED_PARAMETER(debugHandle); SkipCharsDebugLog(debugHandle, size, In); (*In) += size; (*Len) += size; } __inline static void -CodecInsertChars(unsigned long size, unsigned char *buffer, unsigned char **Out, StreamStruct *NextCodec, FILE *debugHandle) +CodecInsertChars(unsigned long size, const char *buffer, unsigned char **Out, StreamStruct *NextCodec, FILE *debugHandle) { + UNUSED_PARAMETER(debugHandle); InsertCharsDebugLog(debugHandle, size, buffer); memcpy(*Out, buffer, size); (*Out) += size; @@ -498,9 +505,9 @@ SendUrlMailto(unsigned char **In, unsigned char **Out, unsigned char *OutEnd, un /* we need to replace the mailto url with our own */ CodecSkipChars(sizeof("mailto:") - 1, In, Len, debugHandle); mailToUrl = (unsigned char *)Codec->URL + strlen(Codec->URL) + 1; - mailToUrlLen = strlen(mailToUrl); + mailToUrlLen = strlen((const char *)mailToUrl); HtmlDecodeFlushOutStream(mailToUrlLen, Out, OutEnd, *Len, Codec, NextCodec); - CodecInsertChars(mailToUrlLen, mailToUrl, Out, NextCodec, debugHandle); + CodecInsertChars(mailToUrlLen, (const char *)mailToUrl, Out, NextCodec, debugHandle); } __inline static void @@ -568,6 +575,11 @@ HtmlExamineUrlUntil(unsigned char endChar, unsigned char **In, unsigned char **O { unsigned char *ptr; + UNUSED_PARAMETER(Out); + UNUSED_PARAMETER(Len); + UNUSED_PARAMETER(NextCodec); + UNUSED_PARAMETER(debugHandle); + if (tolower(**In) != 'h') { if (tolower(**In) != 'm') { if (tolower(**In) != 'j') { @@ -576,7 +588,7 @@ HtmlExamineUrlUntil(unsigned char endChar, unsigned char **In, unsigned char **O } if (*End > (*In + sizeof("javascript:") - 1)) { - if (XplStrNCaseCmp(*In, "javascript:", sizeof("javascript:") - 1) == 0) { + if (XplStrNCaseCmp((const char *)*In, "javascript:", sizeof("javascript:") - 1) == 0) { Codec->State = javascriptState; return(1); } @@ -613,7 +625,7 @@ HtmlExamineUrlUntil(unsigned char endChar, unsigned char **In, unsigned char **O if (Codec->URL) { if (*End > (*In + sizeof("mailto") - 1)) { - if (XplStrNCaseCmp(*In, "mailto:", sizeof("mailto:") - 1) == 0) { + if (XplStrNCaseCmp((const char *)*In, "mailto:", sizeof("mailto:") - 1) == 0) { /* we need to replace the mailto url with our own */ Codec->State = mailtoState; return(1); @@ -653,8 +665,13 @@ HtmlExamineUrlUntil(unsigned char endChar, unsigned char **In, unsigned char **O return(1); } - if (*End > (*In + sizeof("http:") - 1)) { - if (XplStrNCaseCmp(*In, "http:", sizeof("http:") - 1) == 0) { + if (*End > (*In + sizeof("https:") - 1)) { + if (XplStrNCaseCmp((const char *)*In, "https:", sizeof("https:") - 1) == 0) { + Codec->State = httpState; + return(1); + } + + if (XplStrNCaseCmp((const char *)*In, "http:", sizeof("http:") - 1) == 0) { Codec->State = httpState; return(1); } @@ -747,7 +764,7 @@ HTML_Decode(StreamStruct *Codec, StreamStruct *NextCodec) if (closeTagChar || Codec->EOS || ((Codec->Len - Len) > MaxTagLen)) { /* look up tag */ - tagId = BongoKeywordBegins(HtmlTagsIndex, In); + tagId = BongoKeywordBegins(HtmlTagsIndex, (const char *)In); if (tagId == -1) { Codec->State = CODEC_HTML_SEND_TAG; @@ -851,7 +868,7 @@ HTML_Decode(StreamStruct *Codec, StreamStruct *NextCodec) if (equalSignChar || Codec->EOS || ((Codec->Len - Len) > MaxAttributeLen)) { /* look up attribute */ - attributeId = BongoKeywordBegins(HtmlAttributeIndex, In); + attributeId = BongoKeywordBegins(HtmlAttributeIndex, (const char *)In); if (attributeId == -1) { Codec->State = CODEC_HTML_SEND_TAG_ATTRIBUTE_NAME; continue; @@ -1398,7 +1415,7 @@ HTML_Decode(StreamStruct *Codec, StreamStruct *NextCodec) if (closeTagChar || Codec->EOS || ((Codec->Len - Len) > MaxTagLen)) { /* look up tag */ - tagId = BongoKeywordBegins(HtmlTagsIndex, In); + tagId = BongoKeywordBegins(HtmlTagsIndex, (const char *)In); if (tagId == -1) { Codec->State = CODEC_HTML_SUPRESS_TAG; continue; @@ -1487,7 +1504,7 @@ HTML_Decode(StreamStruct *Codec, StreamStruct *NextCodec) case CODEC_HTML_SUPRESS_META_CHARSET: { /* determine if the curent stream content is 'charset' */ if (End > (In + sizeof("charset=\"\"") - 1)) { - if (XplStrNCaseCmp(In, "charset", sizeof("charset") - 1) == 0) { + if (XplStrNCaseCmp((const char *)In, "charset", sizeof("charset") - 1) == 0) { if (In[sizeof("charset") - 1] == '=') { if (In[sizeof("charset=") - 1] != '>') { if (In[sizeof("charset=") - 1] != '\'') { @@ -1620,7 +1637,7 @@ HTML_Decode(StreamStruct *Codec, StreamStruct *NextCodec) tmpChar = *endValue; *endValue = '\0'; - Func = FindCodec(In, FALSE); + Func = FindCodec((const char *)In, FALSE); *endValue = tmpChar; if (Func) { /* the existence of Codec->Charset is ensured in CODEC_HTML_SUPRESS_META */ @@ -1679,7 +1696,7 @@ HTML_Decode(StreamStruct *Codec, StreamStruct *NextCodec) case CODEC_HTML_SKIP_META_CHARSET: { /* determine if the curent stream content is 'charset' */ if (End > (In + sizeof("charset=\"\"") - 1)) { - if (XplStrNCaseCmp(In, "charset", sizeof("charset") - 1) == 0) { + if (XplStrNCaseCmp((const char *)In, "charset", sizeof("charset") - 1) == 0) { if (In[sizeof("charset") - 1] == '=') { if (In[sizeof("charset=") - 1] != '>') { if (In[sizeof("charset=") - 1] != '\'') { @@ -1812,7 +1829,7 @@ HTML_Decode(StreamStruct *Codec, StreamStruct *NextCodec) tmpChar = *endValue; *endValue = '\0'; - Func = FindCodecDecoder(In); + Func = FindCodecDecoder((const char *)In); *endValue = tmpChar; if (Func) { /* the existence of Codec->Charset is ensured in CODEC_HTML_SKIP_META */ @@ -1913,7 +1930,8 @@ HTML_Encode(StreamStruct *Codec, StreamStruct *NextCodec) Go=FALSE; continue; } - /* Fall-through */ + BlockEnd++; + continue; } default: { @@ -1992,7 +2010,7 @@ HTML_Encode(StreamStruct *Codec, StreamStruct *NextCodec) i=strlen(Codec->URL); ptr=(unsigned char *)(Codec->URL)+i+1; - i=strlen(ptr); + i=strlen((const char *)ptr); if (Codec->StreamData2) { Len=strlen(Codec->StreamData2); } @@ -2307,11 +2325,11 @@ HTML_Text_Decode(StreamStruct *Codec, StreamStruct *NextCodec) if (closeTagChar || Codec->EOS || ((Codec->Len - Len) > strlen("Len - Len) > strlen("State = CODEC_HTML_TEXT_SUPRESS_META; continue; } else if ((toupper(*(In + 1)) == 'B') && - ((XplStrNCaseCmp(In, "", strlen("")) == 0) || (XplStrNCaseCmp(In, "", strlen("")) == 0) || (XplStrNCaseCmp((const char *)In, "State = CODEC_HTML_TEXT_SKIP_TAG; continue; } @@ -2390,7 +2408,7 @@ HTML_Text_Decode(StreamStruct *Codec, StreamStruct *NextCodec) case CODEC_HTML_TEXT_SUPRESS_META_CHARSET: { /* determine if the curent stream content is 'charset' */ if (End > (In + sizeof("charset=\"\"") - 1)) { - if (XplStrNCaseCmp(In, "charset", sizeof("charset") - 1) == 0) { + if (XplStrNCaseCmp((const char *)In, "charset", sizeof("charset") - 1) == 0) { if (In[sizeof("charset") - 1] == '=') { if (In[sizeof("charset=") - 1] != '>') { if (In[sizeof("charset=") - 1] != '\'') { @@ -2523,7 +2541,7 @@ HTML_Text_Decode(StreamStruct *Codec, StreamStruct *NextCodec) tmpChar = *endValue; *endValue = '\0'; - Func = FindCodec(In, FALSE); + Func = FindCodec((const char *)In, FALSE); *endValue = tmpChar; if (Func) { /* the existence of Codec->Charset is ensured in CODEC_HTML_TEXT_SUPRESS_META */ @@ -2589,7 +2607,7 @@ HTML_Text_Decode(StreamStruct *Codec, StreamStruct *NextCodec) if ((Codec->Len - Len) > strlen("", strlen("")) == 0)) { + (XplStrNCaseCmp((const char *)In, "", strlen("")) == 0)) { Codec->State = CODEC_HTML_TEXT_SUPRESS_TAG; continue; } diff --git a/src/libs/streamio/codec/hz.h b/src/libs/streamio/codec/hz.h index eeb8753..df662b7 100644 --- a/src/libs/streamio/codec/hz.h +++ b/src/libs/streamio/codec/hz.h @@ -153,7 +153,7 @@ HZ_Encode(StreamStruct *Codec, StreamStruct *NextCodec) UTF8DecodeChar(In, wc, Len, Codec->Len); - if (wc >= 0x0000 && wc < 0x0460) { + if (wc < 0x0460) { summary = &gb2312_uni2indx_page00[(wc>>4)]; } else if (wc >= 0x2000 && wc < 0x2650) { summary = &gb2312_uni2indx_page20[(wc>>4)-0x200]; diff --git a/src/libs/streamio/codec/iso2022_cn.h b/src/libs/streamio/codec/iso2022_cn.h index b32c41d..3a87a94 100644 --- a/src/libs/streamio/codec/iso2022_cn.h +++ b/src/libs/streamio/codec/iso2022_cn.h @@ -152,6 +152,7 @@ ISO2022_CN_Decode(StreamStruct *Codec, StreamStruct *NextCodec) } continue; } + __attribute__((fallthrough)); } default: { @@ -409,7 +410,7 @@ ISO2022_CN_Encode(StreamStruct *Codec, StreamStruct *NextCodec) UTF8DecodeChar(In, wc, Len, Codec->Len); /* Are we GB2312? */ - if (wc >= 0x0000 && wc < 0x0460) { + if (wc < 0x0460) { summary = &gb2312_uni2indx_page00[(wc>>4)]; } else if (wc >= 0x2000 && wc < 0x2650) { summary = &gb2312_uni2indx_page20[(wc>>4)-0x200]; @@ -472,7 +473,7 @@ ISO2022_CN_Encode(StreamStruct *Codec, StreamStruct *NextCodec) /* We're not GB2312; check CNS11643 */ FlushOutStream(4); - if (wc >= 0x0000 && wc < 0x0100) { + if (wc < 0x0100) { summary = &cns11643_inv_uni2indx_page00[(wc>>4)]; } else if (wc >= 0x0200 && wc < 0x03d0) { summary = &cns11643_inv_uni2indx_page02[(wc>>4)-0x020]; diff --git a/src/libs/streamio/codec/iso2022_jp.h b/src/libs/streamio/codec/iso2022_jp.h index 5b6a46d..567855b 100644 --- a/src/libs/streamio/codec/iso2022_jp.h +++ b/src/libs/streamio/codec/iso2022_jp.h @@ -303,7 +303,7 @@ ISO2022_JP_Encode(StreamStruct *Codec, StreamStruct *NextCodec) } else { /* Wasn't JIS 0201, try 0208 */ - if (wc >= 0x0000 && wc < 0x0100) { + if (wc < 0x0100) { summary = &jisx0208_uni2indx_page00[(wc>>4)]; } else if (wc >= 0x0300 && wc < 0x0460) { summary = &jisx0208_uni2indx_page03[(wc>>4)-0x030]; diff --git a/src/libs/streamio/codec/iso2022_kr.h b/src/libs/streamio/codec/iso2022_kr.h index b4e3053..7bfe053 100644 --- a/src/libs/streamio/codec/iso2022_kr.h +++ b/src/libs/streamio/codec/iso2022_kr.h @@ -266,7 +266,7 @@ ISO2022_KR_Encode(StreamStruct *Codec, StreamStruct *NextCodec) /* Decode the UTF char */ UTF8DecodeChar(In, wc, Len, Codec->Len); - if (wc >= 0x0000 && wc < 0x0460) { + if (wc < 0x0460) { summary = &KSC5601_CP00[(wc>>4)]; } else if (wc >= 0x2000 && wc < 0x2670) { summary = &KSC5601_CP20[(wc>>4)-0x200]; diff --git a/src/libs/streamio/codec/q.h b/src/libs/streamio/codec/q.h index 87e9d98..27a81f5 100644 --- a/src/libs/streamio/codec/q.h +++ b/src/libs/streamio/codec/q.h @@ -127,7 +127,7 @@ Q_Decode(StreamStruct *Codec, StreamStruct *NextCodec) return(Len); } -static unsigned char *QHEX = {"0123456789ABCDEF"}; +static const char QHEX[] = "0123456789ABCDEF"; #define MAX_CHARS_PER_LINE 75 /* FIXME: this code is copied straight from the "quoted-printable" encoder. @@ -215,4 +215,3 @@ Q_Encode(StreamStruct *Codec, StreamStruct *NextCodec) return(Len); } - diff --git a/src/libs/streamio/codec/qp.h b/src/libs/streamio/codec/qp.h index 6f112e6..675736a 100644 --- a/src/libs/streamio/codec/qp.h +++ b/src/libs/streamio/codec/qp.h @@ -96,7 +96,7 @@ QP_Decode(StreamStruct *Codec, StreamStruct *NextCodec) return(Len); } -static unsigned char *HEX = {"0123456789ABCDEF"}; +static const char HEX[] = "0123456789ABCDEF"; #define MAX_CHARS_PER_LINE 75 static int @@ -179,4 +179,3 @@ QP_Encode(StreamStruct *Codec, StreamStruct *NextCodec) return(Len); } - diff --git a/src/libs/streamio/codec/rfc1522.h b/src/libs/streamio/codec/rfc1522.h index d8a0193..98b3995 100644 --- a/src/libs/streamio/codec/rfc1522.h +++ b/src/libs/streamio/codec/rfc1522.h @@ -791,7 +791,7 @@ RFC1522_Q_Decode(StreamStruct *Codec, StreamStruct *NextCodec) return(Len); } -static unsigned char *HEX_RFC1522 = {"0123456789ABCDEF"}; +static const char HEX_RFC1522[] = "0123456789ABCDEF"; #define MAX_CHARS_PER_LINE 75 static int @@ -874,7 +874,7 @@ NonEncodedWord: /* This is not an encoded word; pass it through */ /* This code is designed to return to pass through mode as quickly */ /* as possible if the current stream content is not an encoded word */ - unsigned char *Charset; + char *Charset; unsigned char *Encoding; unsigned char *String; unsigned char *End; @@ -890,7 +890,7 @@ NonEncodedWord: /* This is not an encoded word; pass it through */ endPtr = Codec->Start + Codec->Len; ptr = In + 2; - Charset = ptr; + Charset = (char *)ptr; /* Look for next '?' */ while (ptr < endPtr) { @@ -1120,7 +1120,7 @@ RFC1522_Encode(StreamStruct *Codec, StreamStruct *NextCodec) } /* Get the stream to translate the output into the users charset */ - if ((CharsetCodec=GetStream((unsigned char *)Codec->Charset, TRUE))==NULL) { + if ((CharsetCodec=GetStream(Codec->Charset, TRUE))==NULL) { In++; Len++; continue; @@ -1139,7 +1139,7 @@ RFC1522_Encode(StreamStruct *Codec, StreamStruct *NextCodec) FlushOutStream(30); /* Encoding indicator */ - Count=snprintf(Out, OutEnd - Out, "=?%s?%c?", (char *)Codec->Charset, Count>10 ? 'B' : 'Q'); + Count=snprintf((char *)Out, OutEnd - Out, "=?%s?%c?", (const char *)Codec->Charset, Count>10 ? 'B' : 'Q'); Out+=Count; NextCodec->Len+=Count; @@ -1280,7 +1280,7 @@ RFC2047_Name_Encode(StreamStruct *Codec, StreamStruct *NextCodec) FlushOutStream(6); /* Get the stream to translate the output into the users charset */ - if ((CharsetCodec=GetStream((unsigned char *)Codec->Charset, TRUE))==NULL) { + if ((CharsetCodec=GetStream(Codec->Charset, TRUE))==NULL) { In++; Len++; continue; @@ -1298,7 +1298,7 @@ RFC2047_Name_Encode(StreamStruct *Codec, StreamStruct *NextCodec) FlushOutStream(30); /* Encoding indicator */ - Count = snprintf(Out, OutEnd - Out, "=?%s?%c?", (char *)Codec->Charset, nonQtextChars > 10 ? 'B' : 'Q'); + Count = snprintf((char *)Out, OutEnd - Out, "=?%s?%c?", (const char *)Codec->Charset, nonQtextChars > 10 ? 'B' : 'Q'); Out+=Count; NextCodec->Len+=Count; @@ -1342,4 +1342,3 @@ RFC2047_Name_Encode(StreamStruct *Codec, StreamStruct *NextCodec) /* We have to flush the buffer */ return(Codec->Len); } - diff --git a/src/libs/streamio/codec/rfc2231.h b/src/libs/streamio/codec/rfc2231.h index 60cdb92..de26afe 100644 --- a/src/libs/streamio/codec/rfc2231.h +++ b/src/libs/streamio/codec/rfc2231.h @@ -904,7 +904,7 @@ Filename_Encode(StreamStruct *Codec, StreamStruct *NextCodec) unsigned char *OutEnd = NextCodec->End; if (Codec->StreamData) { - count = strlen((unsigned char *)(Codec->StreamData)); + count = strlen((const char *)Codec->StreamData); processed = 0; do { if ((count - processed) < BUFSIZE) { @@ -1091,7 +1091,7 @@ RFC2231_NMAP_Decode(StreamStruct *Codec, StreamStruct *NextCodec) StreamStruct *rfc2047Codec; StreamStruct *transportCodec; StreamStruct *charsetCodec; - unsigned char *charset; + char *charset; unsigned char *lang; unsigned char *encodedWord; unsigned char *nonEncodedWord; @@ -1196,8 +1196,8 @@ RFC2231_NMAP_Decode(StreamStruct *Codec, StreamStruct *NextCodec) /* possibly rfc2231 */ encodedWord = NULL; - charset = In + 2; - lang = charset + 1; + charset = (char *)In + 2; + lang = (unsigned char *)charset + 1; while (lang < endPtr) { if (*lang != '\'') { lang++; @@ -1503,15 +1503,13 @@ RFC2231_Value_Encode(StreamStruct *Codec, StreamStruct *NextCodec) unsigned long attrChars; /* number of attribute-chars as defined by rfc2231 */ BOOL needTransportEncoding; /* the presence of 8 bit chars force this to true */ - unsigned long bytesInWord; /* number of bytes of value string to be sent on this line */ - unsigned long lineCount; /* number of lines already created */ Len = 0; if (Codec->EOS) { /* find the length of the parameter name */ - nameLen = strlen((unsigned char *)(Codec->StreamData)); + nameLen = strlen((const char *)Codec->StreamData); /* Initialize stream variables */ In = Codec->Start; @@ -1790,8 +1788,6 @@ ErrorPath: *Out = '='; Out++; NextCodec->Len++; - bytesInWord = Codec->Len; - FlushOutStream(2); *Out = '"'; Out++; diff --git a/src/libs/streamio/codec/rfc822.h b/src/libs/streamio/codec/rfc822.h index 3613d34..e73ae0e 100644 --- a/src/libs/streamio/codec/rfc822.h +++ b/src/libs/streamio/codec/rfc822.h @@ -147,7 +147,7 @@ RFC822_Header_Value_Decode(StreamStruct *Codec, StreamStruct *NextCodec) if (Codec->StreamData) { *ptr = '\0'; - if (XplStrCaseCmp(Codec->StreamData, In) == 0) { + if (XplStrCaseCmp((const char *)Codec->StreamData, (const char *)In) == 0) { Codec->State = RFC822_HEADER_VALUE_SEND; } else { Codec->State = RFC822_HEADER_VALUE_SKIP; diff --git a/src/libs/streamio/codec/search.h b/src/libs/streamio/codec/search.h index e0ada12..933df55 100644 --- a/src/libs/streamio/codec/search.h +++ b/src/libs/streamio/codec/search.h @@ -33,7 +33,7 @@ WordContains(unsigned char *word, unsigned long wordLen, unsigned char *substrin continue; } - if (XplStrNCaseCmp(word + i, substring, substringLen) != 0) { + if (XplStrNCaseCmp((const char *)word + i, (const char *)substring, substringLen) != 0) { continue; } return(TRUE); @@ -55,18 +55,16 @@ Search_Subword_Decode(StreamStruct *Codec, StreamStruct *NextCodec) unsigned long Len = 0; unsigned long substringLen; unsigned char *substring; - unsigned char *word = NULL; unsigned long wordLen; unsigned long remainingLen; substring = Codec->StreamData; - substringLen = strlen(substring); + substringLen = strlen((const char *)substring); if (substringLen > 0) { for(;;) { if (Len < Codec->Len) { if (isspace(*In)) { - word = NULL; In++; Len++; continue; @@ -161,7 +159,7 @@ Search_Substring_Decode(StreamStruct *Codec, StreamStruct *NextCodec) unsigned long count; substring = Codec->StreamData; - substringLen = strlen(substring); + substringLen = strlen((const char *)substring); if (substringLen > 0) { for(;;) { @@ -177,7 +175,7 @@ Search_Substring_Decode(StreamStruct *Codec, StreamStruct *NextCodec) if (substringLen <= remainingLen) { /* we have enough data in the buffer to verify a match */ - if (XplStrNCaseCmp(cur, substring, substringLen) != 0) { + if (XplStrNCaseCmp((const char *)cur, (const char *)substring, substringLen) != 0) { /* not a match, keep going! */ cur++; continue; @@ -227,7 +225,7 @@ Search_Substring_Decode(StreamStruct *Codec, StreamStruct *NextCodec) continue; } - if (XplStrNCaseCmp(cur, substring, remainingLen) != 0) { + if (XplStrNCaseCmp((const char *)cur, (const char *)substring, remainingLen) != 0) { cur++; continue; } diff --git a/src/libs/streamio/codec/sjis.h b/src/libs/streamio/codec/sjis.h index ad6eab9..c83574b 100644 --- a/src/libs/streamio/codec/sjis.h +++ b/src/libs/streamio/codec/sjis.h @@ -171,7 +171,7 @@ SJIS_Encode(StreamStruct *Codec, StreamStruct *NextCodec) } else { /* Wasn't JIS 0201, try 0208 */ - if (wc >= 0x0000 && wc < 0x0100) { + if (wc < 0x0100) { summary = &jisx0208_uni2indx_page00[(wc>>4)]; } else if (wc >= 0x0300 && wc < 0x0460) { summary = &jisx0208_uni2indx_page03[(wc>>4)-0x030]; diff --git a/src/libs/streamio/streamio.c b/src/libs/streamio/streamio.c index d56c7ea..a86db0f 100644 --- a/src/libs/streamio/streamio.c +++ b/src/libs/streamio/streamio.c @@ -519,7 +519,7 @@ StreamDescStruct StreamList[] = { {"33", SJIS_Decode, SJIS_Encode, FALSE, NULL, }, {"34", SJIS_Decode, SJIS_Encode, FALSE, NULL, }, -{NULL, NULL, NULL, FALSE, } +{NULL, NULL, NULL, FALSE, NULL, } }; #if 0 @@ -541,7 +541,7 @@ SigHandler(int Signal) #endif StreamStruct -*GetStreamEncoder(unsigned char *Charset) +*GetStreamEncoder(const char *Charset) { StreamStruct *Stream; long streamId; @@ -564,7 +564,7 @@ StreamStruct } StreamStruct -*GetStreamDecoder(unsigned char *Charset) +*GetStreamDecoder(const char *Charset) { StreamStruct *Stream; long streamId; @@ -587,7 +587,7 @@ StreamStruct } StreamStruct -*GetStream(unsigned char *Charset, BOOL Encoder) +*GetStream(const char *Charset, BOOL Encoder) { StreamDescStruct *StreamListPtr = StreamList; StreamStruct *Stream; @@ -635,7 +635,7 @@ ReleaseStream(StreamStruct *Stream) StreamCodecFunc -FindCodec(unsigned char *Charset, BOOL Encoder) +FindCodec(const char *Charset, BOOL Encoder) { StreamDescStruct *StreamListPtr = StreamList; unsigned long i = 0; @@ -655,7 +655,7 @@ FindCodec(unsigned char *Charset, BOOL Encoder) } StreamCodecFunc -FindCodecEncoder(unsigned char *Charset) +FindCodecEncoder(const char *Charset) { long codecId; @@ -668,7 +668,7 @@ FindCodecEncoder(unsigned char *Charset) } StreamCodecFunc -FindCodecDecoder(unsigned char *Charset) +FindCodecDecoder(const char *Charset) { long codecId; @@ -737,4 +737,3 @@ StreamioShutdown(void) CodecSearchShutdown(); return(TRUE); } - diff --git a/src/libs/streamio/streamiop.h b/src/libs/streamio/streamiop.h index 75fdcac..976a894 100644 --- a/src/libs/streamio/streamiop.h +++ b/src/libs/streamio/streamiop.h @@ -61,6 +61,7 @@ __inline static BOOL FlushOutStreamEx(StreamStruct *Codec, StreamStruct *NextCodec, unsigned char **Out, unsigned char *OutEnd, unsigned long Space) { + UNUSED_PARAMETER(Codec); if ((*Out + Space) < OutEnd) { return(TRUE); } @@ -247,6 +248,6 @@ typedef unsigned long ucs4_t; typedef unsigned long state_t; typedef struct _conv_t *conv_t; -StreamStruct *GetStream(unsigned char *Charset, BOOL Encoder); +StreamStruct *GetStream(const char *Charset, BOOL Encoder); BOOL ReleaseStream(StreamStruct *Stream); -StreamCodecFunc FindCodec(unsigned char *Charset, BOOL Encoder); +StreamCodecFunc FindCodec(const char *Charset, BOOL Encoder); diff --git a/src/libs/util/bongoagent.c b/src/libs/util/bongoagent.c index 71da430..e8af8b0 100644 --- a/src/libs/util/bongoagent.c +++ b/src/libs/util/bongoagent.c @@ -345,13 +345,10 @@ QueueHandleConnection (void *datap) { ListenCallbackData *data = datap; void *client; - int ccode; char line[CONN_BUFSIZE]; client = MemPrivatePoolGetEntryDirect(data->clientPool, __FILE__, __LINE__); - memset(client, 0, data->clientSize); - if (client == NULL) { XplConsolePrintf("%s: New worker failed to startup; out of memory.\r\n", data->agent->name); @@ -360,8 +357,10 @@ QueueHandleConnection (void *datap) return; } + memset(client, 0, data->clientSize); + if (ConnNegotiate(data->conn, data->agent->sslContext)) { - ccode = data->handler(client, data->conn); + data->handler(client, data->conn); } NMAPSendCommand(data->conn, "QDONE\r\n", 7); @@ -379,19 +378,18 @@ HandleConnection (void *datap) { ListenCallbackData *data = datap; void *client; - int ccode; client = MemPrivatePoolGetEntryDirect(data->clientPool, __FILE__, __LINE__); - memset(client, 0, data->clientSize); - if (client == NULL) { XplConsolePrintf("%s: New worker failed to startup; out of memory.\r\n", data->agent->name); return; } + memset(client, 0, data->clientSize); + if (ConnNegotiate(data->conn, data->agent->sslContext)) { - ccode = data->handler(client, data->conn); + data->handler(client, data->conn); } ConnClose(data->conn); @@ -602,7 +600,7 @@ BongoAgentShutdown(BongoAgent *agent) MemFree((char *)agent->name); } - ConnCloseAll(1); + ConnCloseAll(); LogClose(); @@ -856,4 +854,3 @@ BongoAgentStartMonitor(BongoAgent *agent) XplBeginThread(&agent->monitorID, Monitor, 10240, agent, ccode); } - diff --git a/src/libs/util/bongokeyword.c b/src/libs/util/bongokeyword.c index 3ff0769..040afd6 100644 --- a/src/libs/util/bongokeyword.c +++ b/src/libs/util/bongokeyword.c @@ -117,7 +117,7 @@ KeywordCompareCaseInSensitive(const void *param1, const void *param2) } BongoKeywordIndex * -BongoKeywordIndexCreate(char **strings, unsigned long count, BOOL caseInsensitive) +BongoKeywordIndexCreate(const char *const *strings, unsigned long count, BOOL caseInsensitive) { unsigned long i; BongoKeywordIndex *index; @@ -161,12 +161,12 @@ BongoKeywordIndexCreate(char **strings, unsigned long count, BOOL caseInsensitiv } __inline static long -FindMatch(BongoKeywordIndex *keywordIndex, char *searchString, BOOL beginsWith) +FindMatch(BongoKeywordIndex *keywordIndex, const char *searchString, BOOL beginsWith) { BongoKeyword *keyword; unsigned long row = 0; unsigned long nextRow; - unsigned char *string; + const unsigned char *string; BongoKeywordTable *table; table = keywordIndex->table; @@ -250,13 +250,13 @@ FindMatch(BongoKeywordIndex *keywordIndex, char *searchString, BOOL beginsWith) } long -BongoKeywordFind(BongoKeywordIndex *keywordIndex, unsigned char *searchString) +BongoKeywordFind(BongoKeywordIndex *keywordIndex, const char *searchString) { return(FindMatch(keywordIndex, searchString, FALSE)); } long -BongoKeywordBegins(BongoKeywordIndex *keywordIndex, unsigned char *searchString) +BongoKeywordBegins(BongoKeywordIndex *keywordIndex, const char *searchString) { return(FindMatch(keywordIndex, searchString, TRUE)); } diff --git a/src/libs/util/bongoutil.c b/src/libs/util/bongoutil.c index 5beb669..bf1c02f 100644 --- a/src/libs/util/bongoutil.c +++ b/src/libs/util/bongoutil.c @@ -24,7 +24,7 @@ #include #include -const unsigned char *Base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; +const char Base64Chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="; BOOL QuickNCmp(char *str1, char *str2, int len) @@ -424,23 +424,19 @@ ProtocolCommandTreeRotateRight(ProtocolCommandTree *tree, ProtocolCommand *comma } ProtocolCommand * -ProtocolCommandTreeSearch(ProtocolCommandTree *tree, const unsigned char *command) +ProtocolCommandTreeSearch(ProtocolCommandTree *tree, const char *command) { - const unsigned char *k1; - const unsigned char *k2; + const char *k1; + const char *k2; ProtocolCommand *x; - ProtocolCommand *y; if (tree && command) { - y = tree->sentinel; x = tree->root; k1 = x->name; k2 = command; while (x != tree->sentinel) { - y = x; - - if (*k1 < toupper(*k2)) { + if (*k1 < toupper((unsigned char)*k2)) { x = x->left; k1 = x->name; @@ -449,7 +445,7 @@ ProtocolCommandTreeSearch(ProtocolCommandTree *tree, const unsigned char *comman continue; } - if (*k1 > toupper(*k2)) { + if (*k1 > toupper((unsigned char)*k2)) { x = x->right; k1 = x->name; @@ -475,8 +471,8 @@ ProtocolCommandTreeSearch(ProtocolCommandTree *tree, const unsigned char *comman static void ProtocolCommandTreeInsert(ProtocolCommandTree *tree, ProtocolCommand *command) { - const unsigned char *k1; - const unsigned char *k2; + const char *k1; + const char *k2; BOOL l = FALSE; ProtocolCommand *x; ProtocolCommand *y; @@ -639,13 +635,13 @@ LoadProtocolCommandTree(ProtocolCommandTree *tree, ProtocolCommand *commands) return; } -unsigned char * -DecodeBase64(unsigned char *EncodedString) +char * +DecodeBase64(char *EncodedString) { int i; int j; int length = strlen(EncodedString); - unsigned char *buffer = MemStrdup(EncodedString); + unsigned char *buffer = (unsigned char *)MemStrdup(EncodedString); unsigned char *ptr = buffer; unsigned char table[257]; unsigned char *ptr2; @@ -654,7 +650,7 @@ DecodeBase64(unsigned char *EncodedString) table[256] = '\0'; for (i = 0; i < 65; i++) { - table[Base64Chars[i]] = i; + table[(unsigned char)Base64Chars[i]] = i; } for (i=0; i= NMAP_CRED_STORE_SIZE) { - srcPtr = Credential; - dstPtr = Hash; - dstEnd = Hash + NMAP_HASH_SIZE; + srcPtr = (const unsigned char *)Credential; + dstPtr = (unsigned char *)Hash; + dstEnd = (unsigned char *)Hash + NMAP_HASH_SIZE; XplHashNew(&ctx, XPLHASH_MD5); XplHashWrite(&ctx, srcPtr, NMAP_CRED_CHUNK_SIZE); @@ -844,7 +840,7 @@ HashCredential(unsigned char *Credential, unsigned char *Hash) do { XplHashNew(&ctx, XPLHASH_MD5); XplHashWrite(&ctx, srcPtr, NMAP_CRED_CHUNK_SIZE); - XplHashWrite(&ctx, Hash, dstPtr - Hash); + XplHashWrite(&ctx, Hash, dstPtr - (unsigned char *)Hash); XplHashFinalBytes(&ctx, digest, XPLHASH_MD5BYTES_LENGTH); srcPtr += NMAP_CRED_CHUNK_SIZE; diff --git a/src/libs/util/pools.c b/src/libs/util/pools.c index 9fd2ca0..333be4f 100644 --- a/src/libs/util/pools.c +++ b/src/libs/util/pools.c @@ -11,7 +11,7 @@ struct MemPoolPrivateRef { }; EXPORT void * -MemPrivatePoolAlloc(unsigned char *name, size_t AllocationSize, unsigned int MinAllocCount, unsigned int MaxAllocCount, BOOL Dynamic, BOOL Temporary, PoolEntryCB allocCB, PoolEntryCB freeCB, void *clientData) +MemPrivatePoolAlloc(const char *name, size_t AllocationSize, unsigned int MinAllocCount, unsigned int MaxAllocCount, BOOL Dynamic, BOOL Temporary, PoolEntryCB allocCB, PoolEntryCB freeCB, void *clientData) { struct MemPoolPrivateRef *ref; UNUSED_PARAMETER_REFACTOR(name); diff --git a/src/libs/util/stringbuilder.c b/src/libs/util/stringbuilder.c index 5a26668..40ce2aa 100644 --- a/src/libs/util/stringbuilder.c +++ b/src/libs/util/stringbuilder.c @@ -202,13 +202,11 @@ void BongoStringBuilderAppendF(BongoStringBuilder *sb, const char *format, ...) { char buf[1024]; - int i; va_list ap; va_start(ap, format); - i = XplVsnprintf(buf, 1024, format, ap); + XplVsnprintf(buf, 1024, format, ap); va_end(ap); BongoStringBuilderAppend(sb, buf); } - diff --git a/src/libs/util/utf7mod.c b/src/libs/util/utf7mod.c index cddd9ce..f03fa20 100644 --- a/src/libs/util/utf7mod.c +++ b/src/libs/util/utf7mod.c @@ -620,7 +620,7 @@ ModifiedUtf7ToUtf8(char *utf7Start, unsigned long utf7Len, char *utf8Buffer, uns do { /* find end of 7 bit string */ - ptr = strchr(utf7In, '&'); + ptr = memchr(utf7In, '&', utf7InEnd - utf7In); if (!ptr) { if (Copy7BitString(&utf7In, utf7InEnd - utf7In, &utf8Out, utf8OutEnd)) { return((int)(utf8Out - (unsigned char *)utf8Buffer)); @@ -645,7 +645,7 @@ ModifiedUtf7ToUtf8(char *utf7Start, unsigned long utf7Len, char *utf8Buffer, uns } /* have encoded string; find end */ - ptr = strchr(utf7In, '-'); + ptr = memchr(utf7In, '-', utf7InEnd - utf7In); if (ptr) { ccode = ModifiedBase64ToUtf8(&utf7In, ptr, &utf8Out, utf8OutEnd); if (ccode == UTF7_STATUS_SUCCESS) { @@ -699,7 +699,7 @@ GetUtf8FromModifiedUtf7(char *utf7Start, unsigned long utf7Len, char **result) do { /* find end of 7 bit string */ - ptr = strchr(utf7In, '&'); + ptr = memchr(utf7In, '&', utf7InEnd - utf7In); if (!ptr) { Copy7BitStringNoCheck(&utf7In, utf7InEnd - utf7In, &utf8Out); resultLen = (int)(utf8Out - (unsigned char *)utf8OutBuffer); @@ -730,7 +730,7 @@ GetUtf8FromModifiedUtf7(char *utf7Start, unsigned long utf7Len, char **result) } /* have encoded string; find end */ - ptr = strchr(utf7In, '-'); + ptr = memchr(utf7In, '-', utf7InEnd - utf7In); if (ptr) { ccode = ModifiedBase64ToUtf8NoCheck(&utf7In, ptr, &utf8Out); if (ccode == UTF7_STATUS_SUCCESS) { @@ -787,7 +787,7 @@ Utf8ToModifiedUtf7(char *utf8Start, unsigned long utf8Len, char *utf7Buffer, uns BOOL PassThroughMode; unsigned char octet[6]; - long octetCount = 0; + unsigned long octetCount = 0; utf8In = (unsigned char *)utf8Start; utf8InEnd = utf8In + utf8Len; @@ -909,7 +909,7 @@ GetModifiedUtf7FromUtf8(char *utf8Start, unsigned long utf8Len, char **result) unicode utf16low; BOOL PassThroughMode = TRUE; unsigned char octet[6]; - long octetCount = 0; + unsigned long octetCount = 0; /* Set up in and out buffers */ utf8In = (unsigned char *)utf8Start; diff --git a/src/libs/xpl/dir.c b/src/libs/xpl/dir.c index f5f53cd..47ea717 100644 --- a/src/libs/xpl/dir.c +++ b/src/libs/xpl/dir.c @@ -41,7 +41,11 @@ XplDir return NULL; } - strcpy(newDir->Path, dirname); + if (snprintf(newDir->Path, sizeof(newDir->Path), "%s", dirname) >= (int)sizeof(newDir->Path)) { + closedir(newDir->dirp); + MemFree(newDir); + return NULL; + } newDir->d_size = 0; newDir->d_attr = 0; @@ -61,7 +65,10 @@ XplDir if (dirp->direntp) { dirp->d_name = dirp->direntp->d_name; - sprintf(path, "%s/%s", dirp->Path, dirp->direntp->d_name); + if (snprintf(path, sizeof(path), "%s/%s", dirp->Path, + dirp->direntp->d_name) >= (int)sizeof(path)) { + return NULL; + } if (stat(path, &(dirp->stats)) == 0) { dirp->d_attr = dirp->stats.st_mode; dirp->d_cdatetime = dirp->stats.st_mtime; @@ -103,13 +110,16 @@ XplRemoveDir(const char *dir) if (!strcmp(".", dh->direntp->d_name) || !strcmp("..", dh->direntp->d_name)) continue; - snprintf(path, XPL_MAX_PATH, "%s/%s", dh->Path, dh->direntp->d_name); + if (snprintf(path, sizeof(path), "%s/%s", dh->Path, + dh->direntp->d_name) >= (int)sizeof(path)) { + goto finish; + } if (stat(path, &(dh->stats)) != 0) goto finish; if (S_ISREG(dh->stats.st_mode)) { - unlink(dh->direntp->d_name); + if (unlink(path)) goto finish; } else if (S_ISDIR(dh->stats.st_mode)) { if (XplRemoveDir(path)) goto finish; if (rmdir(path)) goto finish; diff --git a/src/libs/xpl/dns.c b/src/libs/xpl/dns.c index 58f9549..121eefd 100644 --- a/src/libs/xpl/dns.c +++ b/src/libs/xpl/dns.c @@ -44,22 +44,24 @@ _XplDns_ParseName(const unsigned char *response, const int response_len, const u break; } if ((*ptr & 0xc0) == 0xc0) { - int result; + int nested_result; int newptr; // this is a pointer to a previous label newptr = (*ptr++ & 0x3f) * 256; newptr += *ptr++; if (newptr == (ptr - response - 2)) return -4; // cyclical pointer; bad. - result = _XplDns_ParseName(response, response_len, (response + newptr), buffer, buffer_len); - if (result <= 0) return result; + nested_result = _XplDns_ParseName(response, response_len, (response + newptr), buffer, buffer_len); + if (nested_result <= 0) return nested_result; break; } else { // append the next label if we have room in the buffer if ((*ptr + 1) < len) { // FIXME - cast below should be removed somehow. - strncat(buffer, (char *)ptr + 1, *ptr); - strncat(buffer, ".", 1); + size_t used = strlen(buffer); + memcpy(buffer + used, ptr + 1, *ptr); + buffer[used + *ptr] = '.'; + buffer[used + *ptr + 1] = '\0'; len -= *ptr + 1; ptr += *ptr + 1; } else { diff --git a/src/libs/xpl/hash.c b/src/libs/xpl/hash.c index 3629291..e656575 100644 --- a/src/libs/xpl/hash.c +++ b/src/libs/xpl/hash.c @@ -13,7 +13,9 @@ #include #include #include +#if GCRYPT_VERSION_NUMBER < 0x010600 GCRY_THREAD_OPTION_PTHREAD_IMPL; +#endif /** * Initialise the XPL library. Should be called once in each program that @@ -37,9 +39,12 @@ XplInit(void) void XplHashInit(void) { - //const char *gcry_version; + /* Since libgcrypt 1.6 the library uses the platform thread support + * internally and GCRYCTL_SET_THREAD_CBS is obsolete. */ + gcry_check_version(NULL); +#if GCRYPT_VERSION_NUMBER < 0x010600 gcry_control (GCRYCTL_SET_THREAD_CBS, &gcry_threads_pthread); - //gcry_version = gcry_check_version(NULL); +#endif gcry_control (GCRYCTL_DISABLE_SECMEM, 0); gcry_control (GCRYCTL_SET_RANDOM_SEED_FILE, XPL_DEFAULT_RANDSEED_PATH); gcry_control (GCRYCTL_INITIALIZATION_FINISHED, 0); @@ -106,25 +111,26 @@ XplHashFinalBytes(xpl_hash_context *context, unsigned char *buffer, size_t lengt void XplHashFinal(xpl_hash_context *context, xpl_hash_stringcase strcase, char *buffer, size_t length) { - char format[5]; - unsigned char *digest; + const unsigned char *digest; char *p; unsigned int i; - memcpy(format, "%02X\0", 5); - if (strcase == XPLHASH_LOWERCASE) - format[3] = 'x'; + if (!buffer || length == 0) { + gcry_md_close(context->gcrypt_context); + return; + } + digest = gcry_md_read(context->gcrypt_context, 0); + + for (i = 0, p = buffer; digest && i < context->buffer_size && + (size_t)(p - buffer) + 2 < length; i++, p += 2) { + if (strcase == XPLHASH_LOWERCASE) + snprintf(p, length - (size_t)(p - buffer), "%02x", digest[i]); + else + snprintf(p, length - (size_t)(p - buffer), "%02X", digest[i]); + } + *p = '\0'; - digest = MemMalloc(context->buffer_size); - memcpy(digest, gcry_md_read(context->gcrypt_context, 0), context->buffer_size); gcry_md_close(context->gcrypt_context); - - for (i = 0, p = buffer; i < context->buffer_size && p < buffer+length-1; i++, p += 2) { - sprintf((char *)p, format, digest[i]); - } - buffer[length-1] = 0; - - MemFree(digest); } /** diff --git a/src/www/CMakeLists.txt b/src/www/CMakeLists.txt new file mode 100644 index 0000000..80809af --- /dev/null +++ b/src/www/CMakeLists.txt @@ -0,0 +1,46 @@ +configure_file(bongo-web.py.in bongo-web @ONLY) + +find_program(MSGFMT_EXECUTABLE msgfmt REQUIRED) +set(BONGO_WEB_LOCALES de fr pt_BR) +foreach(locale IN LISTS BONGO_WEB_LOCALES) + set(mo_file ${CMAKE_CURRENT_BINARY_DIR}/locale/${locale}/LC_MESSAGES/bongo-web.mo) + add_custom_command(OUTPUT ${mo_file} + COMMAND ${CMAKE_COMMAND} -E make_directory + ${CMAKE_CURRENT_BINARY_DIR}/locale/${locale}/LC_MESSAGES + COMMAND ${MSGFMT_EXECUTABLE} --check -o ${mo_file} + ${CMAKE_SOURCE_DIR}/po/bongo-web/${locale}.po + DEPENDS ${CMAKE_SOURCE_DIR}/po/bongo-web/${locale}.po VERBATIM) + list(APPEND BONGO_WEB_CATALOGS ${mo_file}) + install(FILES ${mo_file} + DESTINATION ${LOCALEDIR}/${locale}/LC_MESSAGES) +endforeach() + +add_custom_target(bongo-web-check ALL + COMMAND ${Python3_EXECUTABLE} -m py_compile + ${CMAKE_CURRENT_BINARY_DIR}/bongo-web + DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/bongo-web ${BONGO_WEB_CATALOGS} + VERBATIM) + +install(PROGRAMS ${CMAKE_CURRENT_BINARY_DIR}/bongo-web + DESTINATION ${SBIN_INSTALL_DIR}) + +install(FILES bongo-web.json + DESTINATION ${SYSCONF_INSTALL_DIR}/bongo) + +install(DIRECTORY bongo_web + DESTINATION ${PYTHON_SITEPACKAGES_PATH} + FILES_MATCHING + PATTERN "__pycache__" EXCLUDE + PATTERN "*.py") + +install(DIRECTORY static/ + DESTINATION ${DATA_INSTALL_DIR}/web) + +if(BUILD_TESTING) + add_test(NAME web-dav-discovery + COMMAND ${CMAKE_COMMAND} -E env + "PYTHONPATH=${CMAKE_CURRENT_SOURCE_DIR}:${CMAKE_SOURCE_DIR}/src/libs/python" + "PYTHONPYCACHEPREFIX=${CMAKE_CURRENT_BINARY_DIR}/pycache" + ${Python3_EXECUTABLE} -m unittest discover + -s ${CMAKE_CURRENT_SOURCE_DIR}/tests -v) +endif() diff --git a/src/www/bongo-web.json b/src/www/bongo-web.json new file mode 100644 index 0000000..632d75b --- /dev/null +++ b/src/www/bongo-web.json @@ -0,0 +1,14 @@ +{ + "listen_address": "127.0.0.1", + "port": 8080, + "public_base_url": "", + "external_https": false, + "allow_basic_auth": true, + "trusted_proxies": ["127.0.0.1", "::1"], + "maximum_request_size": 10485760, + "session_database": "/var/lib/bongo/web/sessions.db", + "task_database": "/var/lib/bongo/web/tasks.db", + "session_lifetime": 3600, + "login_attempts": 5, + "login_window": 300 +} diff --git a/src/www/bongo-web.py.in b/src/www/bongo-web.py.in new file mode 100644 index 0000000..32738c9 --- /dev/null +++ b/src/www/bongo-web.py.in @@ -0,0 +1,903 @@ +#!/usr/bin/env python3 +"""Bongo's HTTP entry point for webmail, administration and DAV. + +The service binds to loopback by default and is intended to run behind a +TLS-terminating reverse proxy. Privileged operations are never performed by +this process. +""" + +from __future__ import annotations + +import argparse +import base64 +import binascii +import json +import gettext +import logging +import os +import posixpath +import re +import ssl +from http import HTTPStatus +from http.cookies import SimpleCookie +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from urllib.parse import parse_qs, urlsplit + +from bongo_web.auth import LoginLimiter, authenticate, revoke, valid_csrf +from bongo_web.calendar_store import (CalendarNotFound, CalendarPathError, + PreconditionFailed, delete_event, + list_events, parse_calendar_path, read_event, + write_event) +from bongo_web.card_store import (CardNotFound, CardPathError, + CardPreconditionFailed, delete_card, + list_cards, parse_card_path, read_card, + write_card) +from bongo_web.dav import (card_multistatus, event_multistatus, multistatus, + options_headers, parse_card_report, parse_report, + requested_properties) +from bongo_web.session import (COOKIE_NAME, SessionStore, + expired_session_cookie, session_cookie) +from bongo_web.tasks import TaskStore +from bongo_web.mail_store import list_conversations, list_folders, read_conversation +from bongo_web.compose import send_message + + +DEFAULT_CONFIG = "@SYSCONF_INSTALL_DIR@/bongo/bongo-web.json" +STATIC_DIR = "@DATA_INSTALL_DIR@/web" +LOCALE_DIR = "@LOCALEDIR@" +SERVER_NAME = "Bongo" +STATIC_FILES = { + "/assets/app.css": ("app.css", "text/css; charset=utf-8"), + "/assets/app.js": ("app.js", "text/javascript; charset=utf-8"), + "/assets/Bongo-logo.png": ("Bongo-logo.png", "image/png"), + "/assets/dragonfly.png": ("dragonfly.png", "image/png"), + "/assets/contact-new.png": ("contact-new.png", "image/png"), + "/assets/favicon.ico": ("favicon.ico", "image/x-icon"), + "/assets/mail-inbox-22.png": ("mail-inbox-22.png", "image/png"), + "/assets/calendar-16.png": ("calendar-16.png", "image/png"), + "/assets/agent-control.png": ("agent-control.png", "image/png"), + "/assets/login-bg-top.png": ("login-bg-top.png", "image/png"), + "/assets/login-bg-middle.png": ("login-bg-middle.png", "image/png"), + "/assets/login-bg-bottom.png": ("login-bg-bottom.png", "image/png"), + "/assets/login-logo.png": ("login-logo.png", "image/png"), + "/assets/bg-page.png": ("bg-page.png", "image/png"), + "/assets/bg-selected.png": ("bg-selected.png", "image/png"), + "/assets/top-gradient.png": ("top-gradient.png", "image/png"), +} +UI_MESSAGES = ("Email address", "Password", "Sign in", "Sign out", "Mail", + "Calendar", "Contacts", "Administration", + "Mail, calendar and contacts", "Login failed.", + "Too many login attempts. Please try again later.", + "Calendar access is available through CalDAV.", + "Contact access is available through CardDAV.", + "The secure webmail connection is being enabled.", + "Administrative functions require a server-side role check.", + "Folder", "Refresh", "(no subject)", "Back", + "Load external images", "[external image blocked]", + "Compose new mail", "From", "To", "Cc", "Bcc", "Subject", + "Message", "Send", "Cancel", "Message sent.", + "The message could not be sent.", "Invalid message fields.", + "Tasks", "No open tasks.", "Ignore", "Disable", "Done", + "Settings", "Personal settings", "Open tasks", "Filters", + "External mail accounts", + "Sender identities", "Two-factor authentication", + "App passwords", "Language and display") +SUPPORTED_LANGUAGES = ("en", "de", "fr", "pt_BR") +ASSET_PATH = re.compile(r"^/assets/(css|img)/([A-Za-z0-9._-]+)$") +ASSET_TYPES = {".css": "text/css; charset=utf-8", ".gif": "image/gif", + ".ico": "image/x-icon", ".jpg": "image/jpeg", + ".jpeg": "image/jpeg", ".png": "image/png"} + + +def load_config(path: str) -> dict: + with open(path, "r", encoding="utf-8") as stream: + config = json.load(stream) + if not isinstance(config, dict): + raise ValueError("web configuration must be a JSON object") + return config + + +class BongoWebServer(ThreadingHTTPServer): + daemon_threads = True + allow_reuse_address = True + + def __init__(self, address, handler, config): + super().__init__(address, handler) + self.config = config + self.sessions = SessionStore( + config.get("session_database", "/var/lib/bongo/web/sessions.db"), + config.get("session_lifetime", 3600)) + self.login_limiter = LoginLimiter(config.get("login_attempts", 5), + config.get("login_window", 300)) + self.tasks = TaskStore(config.get("task_database", + "/var/lib/bongo/web/tasks.db")) + + +class BongoWebHandler(BaseHTTPRequestHandler): + protocol_version = "HTTP/1.1" + server_version = SERVER_NAME + sys_version = "" + + def version_string(self): + return SERVER_NAME + + def log_message(self, message, *args): + logging.info("%s %s", self.client_address[0], message % args) + + def send_response(self, code, message=None): + super().send_response(code, message) + self.send_header("X-Content-Type-Options", "nosniff") + self.send_header("X-Frame-Options", "DENY") + self.send_header("Referrer-Policy", "no-referrer") + self.send_header("Permissions-Policy", "camera=(), microphone=(), geolocation=()") + self.send_header("Content-Security-Policy", "default-src 'none'; script-src 'self'; style-src 'self'; img-src 'self' data: http: https:; connect-src 'self'; frame-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'") + self.send_header("Cache-Control", "no-store") + + def _path(self): + raw_path = urlsplit(self.path).path + collection_path = raw_path.endswith("/") + normalized = posixpath.normpath("/" + raw_path.lstrip("/")) + if collection_path and normalized != "/": + normalized += "/" + return normalized + + def _empty(self, status, headers=None): + self.send_response(status) + for name, value in (headers or {}).items(): + self.send_header(name, value) + self.send_header("Content-Length", "0") + self.end_headers() + + def _json(self, status, value, headers=None): + payload = json.dumps(value, ensure_ascii=False).encode("utf-8") + b"\n" + self.send_response(status) + self.send_header("Content-Type", "application/json; charset=utf-8") + for name, value in (headers or {}).items(): + self.send_header(name, value) + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + + def _static(self, filename, content_type): + try: + with open(os.path.join(STATIC_DIR, filename), "rb") as stream: + payload = stream.read() + except OSError: + self._empty(HTTPStatus.NOT_FOUND) + return + self.send_response(HTTPStatus.OK) + self.send_header("Content-Type", content_type) + self.send_header("Content-Length", str(len(payload))) + self.end_headers() + self.wfile.write(payload) + + def _original_asset(self, path): + match = ASSET_PATH.fullmatch(path) + if match is None: + return False + extension = os.path.splitext(match.group(2))[1].casefold() + content_type = ASSET_TYPES.get(extension) + if content_type is None: + return False + self._static(os.path.join(match.group(1), match.group(2)), content_type) + return True + + def _translations(self): + query = parse_qs(urlsplit(self.path).query) + requested = query.get("lang", [""])[-1].replace("-", "_") + if not requested: + requested = self.headers.get("Accept-Language", "en").split(",", 1)[0] + requested = requested.split(";", 1)[0].strip().replace("-", "_") + language = next((item for item in SUPPORTED_LANGUAGES + if requested == item or requested.split("_", 1)[0] == item), + "en") + catalog = gettext.translation("bongo-web", LOCALE_DIR, + languages=[language], fallback=True) + self._json(HTTPStatus.OK, + {"language": language, + "messages": {message: catalog.gettext(message) + for message in UI_MESSAGES}}) + + def _mail_api(self, path): + session = self._session() + if session is None: + self._json(HTTPStatus.UNAUTHORIZED, + {"error": "authentication required"}, {"Vary": "Cookie"}) + return + try: + if path == "/api/mail/folders": + result = {"folders": list_folders(session["user"], + session["store_cookie"])} + elif path == "/api/mail/conversations": + query = parse_qs(urlsplit(self.path).query) + folder = query.get("folder", ["/mail/INBOX"])[-1] + offset = int(query.get("offset", ["0"])[-1]) + limit = int(query.get("limit", ["30"])[-1]) + result = list_conversations(session["user"], + session["store_cookie"], folder, + offset, limit) + elif path == "/api/mail/conversation": + query = parse_qs(urlsplit(self.path).query) + conversation_id = query.get("id", [""])[-1] + result = read_conversation(session["user"], + session["store_cookie"], + conversation_id) + for message in result.get("messages", ()): + mailing_list = message.get("mailing_list") + if mailing_list: + self.server.tasks.upsert( + session["user"], "mailing_list", mailing_list["id"], + "Mailing list detected", + {"list_id": mailing_list["id"], + "list_post": mailing_list.get("post", ""), + "conversation_id": conversation_id}) + else: + self._empty(HTTPStatus.NOT_FOUND) + return + except FileNotFoundError: + self._empty(HTTPStatus.NOT_FOUND) + return + except (TypeError, ValueError): + self._empty(HTTPStatus.BAD_REQUEST) + return + except RuntimeError: + logging.exception("Could not serve webmail request") + self._empty(HTTPStatus.INTERNAL_SERVER_ERROR) + return + self._json(HTTPStatus.OK, result, {"Vary": "Cookie"}) + + def _session_token(self): + try: + cookies = SimpleCookie(self.headers.get("Cookie", "")) + except Exception: + return None + morsel = cookies.get(COOKIE_NAME) + return morsel.value if morsel else None + + def _session(self): + return self.server.sessions.get(self._session_token()) + + def _request_data(self): + length = self.headers.get("Content-Length") + if length is None: + raise ValueError("length required") + try: + size = int(length, 10) + except ValueError as error: + raise ValueError("invalid length") from error + maximum = int(self.server.config.get("maximum_request_size", 10485760)) + if size < 0 or size > maximum: + raise OverflowError("request too large") + body = self.rfile.read(size) + content_type = self.headers.get_content_type() + if content_type == "application/json": + value = json.loads(body.decode("utf-8")) + if not isinstance(value, dict): + raise ValueError("JSON object required") + return value + if content_type == "application/x-www-form-urlencoded": + values = parse_qs(body.decode("utf-8"), keep_blank_values=True) + return {key: items[-1] for key, items in values.items()} + raise TypeError("unsupported media type") + + def _request_body(self, maximum=65536): + length = self.headers.get("Content-Length", "0") + try: + size = int(length, 10) + except ValueError as error: + raise ValueError("invalid length") from error + if size < 0 or size > maximum: + raise OverflowError("request too large") + return self.rfile.read(size) + + def _dav_session(self): + session = self._session() + if session is not None: + session["transient"] = False + return session + authorization = self.headers.get("Authorization", "") + basic_allowed = (self.server.config.get("allow_basic_auth", True) + and self.server.config.get("external_https", False)) + if basic_allowed and authorization.startswith("Basic "): + try: + decoded = base64.b64decode(authorization[6:], validate=True) + user, password = decoded.decode("utf-8").split(":", 1) + except (binascii.Error, UnicodeError, ValueError): + user = password = "" + lifetime = min(int(self.server.config.get("session_lifetime", 3600)), + 300) + store_cookie = authenticate(user.strip(), password, lifetime) + password = None + if store_cookie is not None: + return {"user": user.strip(), "store_cookie": store_cookie, + "transient": True} + self._json(HTTPStatus.UNAUTHORIZED, + {"error": "authentication required"}, + {"Vary": "Cookie", + "WWW-Authenticate": 'Basic realm="Bongo", charset="UTF-8"'}) + return None + + @staticmethod + def _release_dav_session(session): + if session and session.get("transient"): + revoke(session["user"], session["store_cookie"]) + + def _dav_write_allowed(self, session): + return (session.get("transient") + or valid_csrf(session, self.headers.get("X-Bongo-CSRF", ""))) + + def _dav_redirect(self, service): + base = self.server.config.get("public_base_url", "").rstrip("/") + location = f"{base}/{service}/" if base else f"/{service}/" + self._empty(HTTPStatus.TEMPORARY_REDIRECT, {"Location": location}) + + def do_GET(self): + path = self._path() + if path == "/health": + self._json(HTTPStatus.OK, {"status": "ok"}) + elif path == "/api/session": + session = self._session() + if session is None: + self._json(HTTPStatus.UNAUTHORIZED, {"error": "authentication required"}, + {"Vary": "Cookie"}) + else: + self._json(HTTPStatus.OK, + {"user": session["user"], + "csrf_token": session["csrf_token"]}, + {"Vary": "Cookie"}) + elif path == "/api/i18n": + self._translations() + elif path.startswith("/api/mail/"): + self._mail_api(path) + elif path == "/api/tasks": + session = self._session() + if session is None: + self._json(HTTPStatus.UNAUTHORIZED, + {"error": "authentication required"}) + else: + query = parse_qs(urlsplit(self.path).query) + include_closed = query.get("include_closed", ["0"])[-1] == "1" + self._json(HTTPStatus.OK, + {"tasks": self.server.tasks.list( + session["user"], include_closed=include_closed)}, + {"Vary": "Cookie"}) + elif path == "/.well-known/caldav": + self._dav_redirect("calendars") + elif path == "/.well-known/carddav": + self._dav_redirect("addressbooks") + elif path.startswith("/calendars/") and path.endswith(".ics"): + self._calendar_get(path) + elif path.startswith("/addressbooks/") and path.endswith(".vcf"): + self._card_get(path) + elif path in ("/", "/mail", "/admin", "/settings"): + self._static("index.html", "text/html; charset=utf-8") + elif path in STATIC_FILES: + self._static(*STATIC_FILES[path]) + elif path.startswith("/assets/") and self._original_asset(path): + pass + elif path in ("/calendars", "/addressbooks"): + self._dav_redirect(path[1:]) + else: + self._empty(HTTPStatus.NOT_FOUND) + + def _calendar_get(self, path): + session = self._dav_session() + if session is None: + return + try: + try: + resource = parse_calendar_path(path, session["user"]) + except PermissionError: + self._empty(HTTPStatus.FORBIDDEN) + return + except (CalendarNotFound, CalendarPathError): + self._empty(HTTPStatus.NOT_FOUND) + return + if resource["kind"] != "event": + self._empty(HTTPStatus.NOT_FOUND) + return + try: + payload, etag = read_event(session["user"], + session["store_cookie"], + resource["event"]) + except CalendarNotFound: + self._empty(HTTPStatus.NOT_FOUND) + return + if self.headers.get("If-None-Match") == etag: + self._empty(HTTPStatus.NOT_MODIFIED, {"ETag": etag}) + return + self.send_response(HTTPStatus.OK) + self.send_header("Content-Type", "text/calendar; charset=utf-8") + self.send_header("ETag", etag) + self.send_header("Content-Length", str(len(payload))) + self.send_header("Vary", "Cookie, Authorization") + self.end_headers() + self.wfile.write(payload) + finally: + self._release_dav_session(session) + + def _card_get(self, path): + session = self._dav_session() + if session is None: + return + try: + try: + resource = parse_card_path(path, session["user"]) + if resource["kind"] != "card": + raise CardPathError("not a card") + payload, etag = read_card(session["user"], + session["store_cookie"], + resource["card"]) + except PermissionError: + self._empty(HTTPStatus.FORBIDDEN) + return + except (CardNotFound, CardPathError): + self._empty(HTTPStatus.NOT_FOUND) + return + except RuntimeError: + logging.exception("Could not read CardDAV contact") + self._empty(HTTPStatus.INTERNAL_SERVER_ERROR) + return + if self.headers.get("If-None-Match") == etag: + self._empty(HTTPStatus.NOT_MODIFIED, {"ETag": etag}) + return + self.send_response(HTTPStatus.OK) + self.send_header("Content-Type", "text/vcard; charset=utf-8") + self.send_header("ETag", etag) + self.send_header("Content-Length", str(len(payload))) + self.send_header("Vary", "Cookie, Authorization") + self.end_headers() + self.wfile.write(payload) + finally: + self._release_dav_session(session) + + def do_HEAD(self): + path = self._path() + if path == "/health": + self._empty(HTTPStatus.OK) + else: + self._empty(HTTPStatus.NOT_FOUND) + + def do_POST(self): + path = self._path() + if path == "/api/tasks/state": + self._task_state() + return + if path == "/api/mail/send": + self._mail_send() + return + if path != "/api/session": + self._empty(HTTPStatus.NOT_FOUND) + return + try: + data = self._request_data() + except OverflowError: + self._empty(HTTPStatus.REQUEST_ENTITY_TOO_LARGE) + return + except TypeError: + self._empty(HTTPStatus.UNSUPPORTED_MEDIA_TYPE) + return + except (UnicodeError, ValueError, json.JSONDecodeError): + self._empty(HTTPStatus.BAD_REQUEST) + return + user = str(data.get("user", "")).strip() + password = str(data.get("password", "")) + key = (self.client_address[0], user.casefold()) + if not self.server.login_limiter.allowed(key): + self._json(HTTPStatus.TOO_MANY_REQUESTS, + {"error": "authentication temporarily unavailable"}, + {"Retry-After": str(self.server.login_limiter.window)}) + return + lifetime = int(self.server.config.get("session_lifetime", 3600)) + store_cookie = authenticate(user, password, lifetime) + password = None + if store_cookie is None: + self.server.login_limiter.failed(key) + self._json(HTTPStatus.UNAUTHORIZED, {"error": "authentication failed"}) + return + self.server.login_limiter.succeeded(key) + token, csrf_token = self.server.sessions.create(user, store_cookie) + self._json(HTTPStatus.CREATED, + {"user": user, "csrf_token": csrf_token}, + {"Set-Cookie": session_cookie(token, lifetime), + "Vary": "Cookie"}) + + def _mail_send(self): + session = self._session() + if session is None: + self._json(HTTPStatus.UNAUTHORIZED, + {"error": "authentication required"}, {"Vary": "Cookie"}) + return + if not valid_csrf(session, self.headers.get("X-Bongo-CSRF", "")): + self._json(HTTPStatus.FORBIDDEN, {"error": "invalid CSRF token"}) + return + try: + data = self._request_data() + send_message(session["user"], data) + except OverflowError: + self._empty(HTTPStatus.REQUEST_ENTITY_TOO_LARGE) + return + except TypeError: + self._empty(HTTPStatus.UNSUPPORTED_MEDIA_TYPE) + return + except (UnicodeError, ValueError, json.JSONDecodeError): + self._json(HTTPStatus.BAD_REQUEST, {"error": "invalid message"}) + return + except Exception: + logging.exception("Could not submit webmail message") + self._json(HTTPStatus.BAD_GATEWAY, {"error": "queue unavailable"}) + return + self._empty(HTTPStatus.NO_CONTENT, {"Vary": "Cookie"}) + + def _task_state(self): + session = self._session() + if session is None: + self._json(HTTPStatus.UNAUTHORIZED, + {"error": "authentication required"}) + return + if not valid_csrf(session, self.headers.get("X-Bongo-CSRF", "")): + self._json(HTTPStatus.FORBIDDEN, {"error": "invalid CSRF token"}) + return + try: + data = self._request_data() + self.server.tasks.set_state(session["user"], data.get("id"), + str(data.get("state", ""))) + except FileNotFoundError: + self._empty(HTTPStatus.NOT_FOUND) + return + except PermissionError: + self._empty(HTTPStatus.FORBIDDEN) + return + except (TypeError, ValueError, OverflowError, json.JSONDecodeError): + self._empty(HTTPStatus.BAD_REQUEST) + return + self._empty(HTTPStatus.NO_CONTENT, {"Vary": "Cookie"}) + + def do_DELETE(self): + path = self._path() + if path.startswith("/calendars/") and path.endswith(".ics"): + self._calendar_delete(path) + return + if path.startswith("/addressbooks/") and path.endswith(".vcf"): + self._card_delete(path) + return + if path != "/api/session": + self._empty(HTTPStatus.NOT_FOUND) + return + token = self._session_token() + session = self.server.sessions.get(token) + if session is None: + self._json(HTTPStatus.UNAUTHORIZED, {"error": "authentication required"}) + return + if not valid_csrf(session, self.headers.get("X-Bongo-CSRF", "")): + self._json(HTTPStatus.FORBIDDEN, {"error": "invalid CSRF token"}) + return + self.server.sessions.delete(token) + revoke(session["user"], session["store_cookie"]) + self._empty(HTTPStatus.NO_CONTENT, + {"Set-Cookie": expired_session_cookie(), "Vary": "Cookie"}) + + def do_PUT(self): + path = self._path() + if path.startswith("/addressbooks/") and path.endswith(".vcf"): + self._card_put(path) + return + if not (path.startswith("/calendars/") and path.endswith(".ics")): + self._empty(HTTPStatus.NOT_FOUND) + return + session = self._dav_session() + if session is None: + return + try: + if not self._dav_write_allowed(session): + self._json(HTTPStatus.FORBIDDEN, {"error": "invalid CSRF token"}) + return + try: + resource = parse_calendar_path(path, session["user"]) + except PermissionError: + self._empty(HTTPStatus.FORBIDDEN) + return + except (CalendarNotFound, CalendarPathError): + self._empty(HTTPStatus.NOT_FOUND) + return + if self.headers.get_content_type() != "text/calendar": + self._empty(HTTPStatus.UNSUPPORTED_MEDIA_TYPE) + return + try: + payload = self._request_body(min( + int(self.server.config.get("maximum_request_size", 10485760)), + 1048576)) + created, etag = write_event( + session["user"], session["store_cookie"], resource["event"], + payload, self.headers.get("If-Match"), + self.headers.get("If-None-Match")) + except OverflowError: + self._empty(HTTPStatus.REQUEST_ENTITY_TOO_LARGE) + return + except (UnicodeError, ValueError): + self._empty(HTTPStatus.BAD_REQUEST) + return + except PreconditionFailed: + self._empty(HTTPStatus.PRECONDITION_FAILED) + return + except RuntimeError: + logging.exception("Could not store CalDAV event") + self._empty(HTTPStatus.INTERNAL_SERVER_ERROR) + return + self._empty(HTTPStatus.CREATED if created else HTTPStatus.NO_CONTENT, + {"ETag": etag}) + finally: + self._release_dav_session(session) + + def _card_put(self, path): + session = self._dav_session() + if session is None: + return + try: + if not self._dav_write_allowed(session): + self._json(HTTPStatus.FORBIDDEN, {"error": "invalid CSRF token"}) + return + try: + resource = parse_card_path(path, session["user"]) + except PermissionError: + self._empty(HTTPStatus.FORBIDDEN) + return + except (CardNotFound, CardPathError): + self._empty(HTTPStatus.NOT_FOUND) + return + if self.headers.get_content_type() not in ("text/vcard", + "text/x-vcard"): + self._empty(HTTPStatus.UNSUPPORTED_MEDIA_TYPE) + return + try: + payload = self._request_body(min( + int(self.server.config.get("maximum_request_size", 10485760)), + 1048576)) + created, etag = write_card( + session["user"], session["store_cookie"], resource["card"], + payload, self.headers.get("If-Match"), + self.headers.get("If-None-Match")) + except OverflowError: + self._empty(HTTPStatus.REQUEST_ENTITY_TOO_LARGE) + return + except (UnicodeError, ValueError): + self._empty(HTTPStatus.BAD_REQUEST) + return + except CardPreconditionFailed: + self._empty(HTTPStatus.PRECONDITION_FAILED) + return + except RuntimeError: + logging.exception("Could not store CardDAV contact") + self._empty(HTTPStatus.INTERNAL_SERVER_ERROR) + return + self._empty(HTTPStatus.CREATED if created else HTTPStatus.NO_CONTENT, + {"ETag": etag}) + finally: + self._release_dav_session(session) + + def _card_delete(self, path): + session = self._dav_session() + if session is None: + return + try: + if not self._dav_write_allowed(session): + self._json(HTTPStatus.FORBIDDEN, {"error": "invalid CSRF token"}) + return + try: + resource = parse_card_path(path, session["user"]) + delete_card(session["user"], session["store_cookie"], + resource["card"], self.headers.get("If-Match")) + except PermissionError: + self._empty(HTTPStatus.FORBIDDEN) + return + except (CardNotFound, CardPathError): + self._empty(HTTPStatus.NOT_FOUND) + return + except CardPreconditionFailed: + self._empty(HTTPStatus.PRECONDITION_FAILED) + return + except RuntimeError: + logging.exception("Could not delete CardDAV contact") + self._empty(HTTPStatus.INTERNAL_SERVER_ERROR) + return + self._empty(HTTPStatus.NO_CONTENT) + finally: + self._release_dav_session(session) + + def _calendar_delete(self, path): + session = self._dav_session() + if session is None: + return + try: + if not self._dav_write_allowed(session): + self._json(HTTPStatus.FORBIDDEN, {"error": "invalid CSRF token"}) + return + try: + resource = parse_calendar_path(path, session["user"]) + delete_event(session["user"], session["store_cookie"], + resource["event"], self.headers.get("If-Match")) + except PermissionError: + self._empty(HTTPStatus.FORBIDDEN) + return + except (CalendarNotFound, CalendarPathError): + self._empty(HTTPStatus.NOT_FOUND) + return + except PreconditionFailed: + self._empty(HTTPStatus.PRECONDITION_FAILED) + return + except RuntimeError: + logging.exception("Could not delete CalDAV event") + self._empty(HTTPStatus.INTERNAL_SERVER_ERROR) + return + self._empty(HTTPStatus.NO_CONTENT) + finally: + self._release_dav_session(session) + + def do_OPTIONS(self): + path = self._path() + if path.startswith(("/calendars/", "/addressbooks/", "/principals/")): + session = self._dav_session() + if session is None: + return + try: + self._empty(HTTPStatus.OK, options_headers()) + finally: + self._release_dav_session(session) + else: + self._empty(HTTPStatus.NO_CONTENT, + {"Allow": "OPTIONS, GET, HEAD, POST, DELETE"}) + + def do_PROPFIND(self): + session = self._dav_session() + if session is None: + return + try: + depth_value = self.headers.get("Depth", "0") + if depth_value not in ("0", "1"): + self._empty(HTTPStatus.FORBIDDEN) + return + try: + requested = requested_properties(self._request_body()) + except OverflowError: + self._empty(HTTPStatus.REQUEST_ENTITY_TOO_LARGE) + return + except (UnicodeError, ValueError): + self._empty(HTTPStatus.BAD_REQUEST) + return + payload = multistatus(self._path(), session["user"], + int(depth_value), requested) + if payload is None: + self._empty(HTTPStatus.NOT_FOUND) + return + self.send_response(HTTPStatus.MULTI_STATUS) + self.send_header("Content-Type", "application/xml; charset=utf-8") + self.send_header("Content-Length", str(len(payload))) + self.send_header("Vary", "Cookie") + self.end_headers() + self.wfile.write(payload) + finally: + self._release_dav_session(session) + + def do_REPORT(self): + session = self._dav_session() + if session is None: + return + try: + path = self._path() + if path.startswith("/addressbooks/"): + self._card_report(session, path) + return + try: + resource = parse_calendar_path(self._path(), session["user"]) + except PermissionError: + self._empty(HTTPStatus.FORBIDDEN) + return + except (CalendarNotFound, CalendarPathError): + self._empty(HTTPStatus.NOT_FOUND) + return + if resource["kind"] != "calendar": + self._empty(HTTPStatus.NOT_FOUND) + return + try: + report = parse_report(self._request_body()) + events = list_events(session["user"], session["store_cookie"], + report["start"], report["end"]) + except OverflowError: + self._empty(HTTPStatus.REQUEST_ENTITY_TOO_LARGE) + return + except (UnicodeError, ValueError): + self._empty(HTTPStatus.BAD_REQUEST) + return + except CalendarNotFound: + self._empty(HTTPStatus.NOT_FOUND) + return + except RuntimeError: + logging.exception("Could not query CalDAV events") + self._empty(HTTPStatus.INTERNAL_SERVER_ERROR) + return + requested_hrefs = report["hrefs"] if report["kind"] == "multiget" \ + else None + payload = event_multistatus(events, report["properties"], + requested_hrefs) + self.send_response(HTTPStatus.MULTI_STATUS) + self.send_header("Content-Type", "application/xml; charset=utf-8") + self.send_header("Content-Length", str(len(payload))) + self.send_header("Vary", "Cookie, Authorization") + self.end_headers() + self.wfile.write(payload) + finally: + self._release_dav_session(session) + + def _card_report(self, session, path): + try: + resource = parse_card_path(path, session["user"]) + except PermissionError: + self._empty(HTTPStatus.FORBIDDEN) + return + except (CardNotFound, CardPathError): + self._empty(HTTPStatus.NOT_FOUND) + return + if resource["kind"] != "addressbook": + self._empty(HTTPStatus.NOT_FOUND) + return + try: + report = parse_card_report(self._request_body()) + cards = list_cards(session["user"], session["store_cookie"]) + except OverflowError: + self._empty(HTTPStatus.REQUEST_ENTITY_TOO_LARGE) + return + except (UnicodeError, ValueError): + self._empty(HTTPStatus.BAD_REQUEST) + return + except RuntimeError: + logging.exception("Could not query CardDAV contacts") + self._empty(HTTPStatus.INTERNAL_SERVER_ERROR) + return + requested_hrefs = report["hrefs"] if report["kind"] == "multiget" \ + else None + payload = card_multistatus(cards, report["properties"], requested_hrefs) + self.send_response(HTTPStatus.MULTI_STATUS) + self.send_header("Content-Type", "application/xml; charset=utf-8") + self.send_header("Content-Length", str(len(payload))) + self.send_header("Vary", "Cookie, Authorization") + self.end_headers() + self.wfile.write(payload) + + +def parse_args(): + parser = argparse.ArgumentParser(description="Bongo web and DAV service") + parser.add_argument("--config", default=DEFAULT_CONFIG) + parser.add_argument("--listen-address") + parser.add_argument("--port", type=int) + parser.add_argument("--tls-certificate") + parser.add_argument("--tls-key") + parser.add_argument("--verbose", action="store_true") + return parser.parse_args() + + +def main(): + args = parse_args() + logging.basicConfig(level=logging.DEBUG if args.verbose else logging.INFO, + format="%(asctime)s %(levelname)s %(message)s") + config = load_config(args.config) + address = args.listen_address or config.get("listen_address", "127.0.0.1") + port = args.port or int(config.get("port", 8080)) + server = BongoWebServer((address, port), BongoWebHandler, config) + if bool(args.tls_certificate) != bool(args.tls_key): + raise SystemExit("both --tls-certificate and --tls-key are required") + if args.tls_certificate: + context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) + context.minimum_version = ssl.TLSVersion.TLSv1_2 + context.load_cert_chain(args.tls_certificate, args.tls_key) + server.socket = context.wrap_socket(server.socket, server_side=True) + logging.info("Bongo web service listening on %s:%d", address, port) + try: + server.serve_forever() + except KeyboardInterrupt: + pass + finally: + server.server_close() + + +if __name__ == "__main__": + main() diff --git a/src/www/bongo_web/__init__.py b/src/www/bongo_web/__init__.py new file mode 100644 index 0000000..018eef0 --- /dev/null +++ b/src/www/bongo_web/__init__.py @@ -0,0 +1 @@ +"""Security-sensitive support code for the Bongo web service.""" diff --git a/src/www/bongo_web/auth.py b/src/www/bongo_web/auth.py new file mode 100644 index 0000000..d196b12 --- /dev/null +++ b/src/www/bongo_web/auth.py @@ -0,0 +1,73 @@ +"""Authentication adapter between HTTP and the Bongo Store.""" + +import hmac +import logging +import threading +import time + + +class LoginLimiter: + def __init__(self, attempts=5, window=300): + self.attempts = int(attempts) + self.window = int(window) + self.failures = {} + self.lock = threading.Lock() + + def allowed(self, key): + now = time.monotonic() + with self.lock: + recent = [stamp for stamp in self.failures.get(key, ()) + if now - stamp < self.window] + self.failures[key] = recent + return len(recent) < self.attempts + + def failed(self, key): + with self.lock: + self.failures.setdefault(key, []).append(time.monotonic()) + + def succeeded(self, key): + with self.lock: + self.failures.pop(key, None) + + +def authenticate(user, password, lifetime): + if not user or not password or len(user) > 320 or len(password) > 4096: + return None + client = None + try: + from bongo.store.StoreClient import StoreClient + client = StoreClient(user, user, authPassword=password) + return client.CookieNew(int(lifetime)) + except Exception: + logging.info("Bongo Store authentication failed for user %r", user) + return None + finally: + if client is not None: + try: + client.Quit() + except Exception: + logging.warning("Could not close Store connection after login") + + +def revoke(user, store_cookie): + """Best-effort revocation of a server-side Bongo Store cookie.""" + if not user or not store_cookie: + return + client = None + try: + from bongo.store.StoreClient import StoreClient + client = StoreClient(user, user, authCookie=store_cookie) + client.CookieDelete(store_cookie) + except Exception: + logging.warning("Could not revoke Store cookie for user %r", user) + finally: + if client is not None: + try: + client.Quit() + except Exception: + logging.warning("Could not close Store connection after logout") + + +def valid_csrf(session, supplied): + expected = session.get("csrf_token", "") if session else "" + return bool(supplied and hmac.compare_digest(expected, supplied)) diff --git a/src/www/bongo_web/calendar_store.py b/src/www/bongo_web/calendar_store.py new file mode 100644 index 0000000..e0ece14 --- /dev/null +++ b/src/www/bongo_web/calendar_store.py @@ -0,0 +1,202 @@ +"""CalDAV-to-Bongo Store mapping.""" + +import hashlib +import re +from urllib.parse import quote, unquote + + +EVENT_NAME = re.compile(r"^[A-Za-z0-9._~-]{1,240}\.ics$") +EVENT_DOCUMENT_TYPE = 0x0003 + + +class CalendarPathError(ValueError): + pass + + +class CalendarNotFound(LookupError): + pass + + +class PreconditionFailed(RuntimeError): + pass + + +def parse_calendar_path(path, authenticated_user): + parts = path.strip("/").split("/") + if len(parts) not in (3, 4) or parts[0] != "calendars": + raise CalendarPathError("not a calendar resource") + user = unquote(parts[1]) + if user != authenticated_user: + raise PermissionError("calendar owner differs from authenticated user") + if unquote(parts[2]) != "personal": + raise CalendarNotFound("calendar does not exist") + if len(parts) == 3: + return {"kind": "calendar", "user": user, "calendar": "personal"} + event = unquote(parts[3]) + if not EVENT_NAME.fullmatch(event): + raise CalendarPathError("invalid event resource name") + return {"kind": "event", "user": user, "calendar": "personal", + "event": event, "store_path": "/events/" + event[:-4]} + + +def event_href(user, event): + return "/calendars/{}/personal/{}".format( + quote(user, safe="@.-_~"), quote(event, safe="._~-")) + + +def open_store(user, store_cookie): + from bongo.store.StoreClient import StoreClient + return StoreClient(user, user, authCookie=store_cookie) + + +def json_to_ical(document): + from libbongo._calendar import bongojson, cal + value = bongojson.ParseString(document.strip()) + if not value: + raise CalendarNotFound("invalid stored calendar object") + output = cal.JsonToIcal(value) + return output.encode("utf-8") if isinstance(output, str) else bytes(output) + + +def ical_to_json(payload): + if len(payload) > 1048576: + raise ValueError("calendar object is too large") + text = payload.decode("utf-8", "strict") + upper = text.upper() + if (upper.count("BEGIN:VCALENDAR") != 1 + or upper.count("END:VCALENDAR") != 1 + or upper.count("BEGIN:VEVENT") != 1 + or upper.count("END:VEVENT") != 1 + or "BEGIN:VTODO" in upper + or "\nUID:" not in upper.replace("\r\n", "\n")): + raise ValueError("exactly one VEVENT with UID is required") + from libbongo._calendar import cal + converted = cal.IcalToJson(text) + if converted is None: + raise ValueError("iCalendar conversion failed") + return str(converted) + + +def read_event(user, store_cookie, event): + client = None + try: + client = open_store(user, store_cookie) + document = client.Read("/events/" + event[:-4]) + payload = json_to_ical(document) + except CalendarNotFound: + raise + except Exception as error: + # Store command failures intentionally become a non-enumerating 404. + raise CalendarNotFound("event not found") from error + finally: + if client is not None: + try: + client.Quit() + except Exception: + pass + return payload, '"{}"'.format(hashlib.sha256(payload).hexdigest()) + + +def make_etag(payload): + return '"{}"'.format(hashlib.sha256(payload).hexdigest()) + + +def etag_matches(condition, current): + if not condition: + return False + return condition.strip() == "*" or current in { + item.strip() for item in condition.split(",") + } + + +def _close(client): + if client is not None: + try: + client.Quit() + except Exception: + pass + + +def write_event(user, store_cookie, event, payload, if_match=None, + if_none_match=None, client_factory=open_store, + to_json=ical_to_json, to_ical=json_to_ical): + """Create or replace one event and return ``(created, etag)``.""" + document = to_json(payload) + path = "/events/" + event[:-4] + client = None + try: + client = client_factory(user, store_cookie) + exists = client.Info(path) is not None + current_etag = None + if exists and (if_match or if_none_match): + current_etag = make_etag(to_ical(client.Read(path))) + if (if_none_match and exists + and etag_matches(if_none_match, current_etag)): + raise PreconditionFailed("event already exists") + if if_match and (not exists or not etag_matches(if_match, current_etag)): + raise PreconditionFailed("event changed") + if exists: + client.Replace(path, document) + else: + client.Write("/events", EVENT_DOCUMENT_TYPE, document, + filename=event[:-4], link="/calendars/personal") + return not exists, make_etag(to_ical(document)) + except (PreconditionFailed, ValueError): + raise + except Exception as error: + raise RuntimeError("could not store calendar event") from error + finally: + _close(client) + + +def delete_event(user, store_cookie, event, if_match=None, + client_factory=open_store, to_ical=json_to_ical): + path = "/events/" + event[:-4] + client = None + try: + client = client_factory(user, store_cookie) + if client.Info(path) is None: + raise CalendarNotFound("event not found") + if if_match: + current_etag = make_etag(to_ical(client.Read(path))) + if not etag_matches(if_match, current_etag): + raise PreconditionFailed("event changed") + client.Delete(path) + except (CalendarNotFound, PreconditionFailed): + raise + except Exception as error: + raise RuntimeError("could not delete calendar event") from error + finally: + _close(client) + + +def list_events(user, store_cookie, start=None, end=None, + client_factory=open_store, to_ical=json_to_ical): + client = None + try: + client = client_factory(user, store_cookie) + calendar = client.Info("/calendars/personal") + if calendar is None: + raise CalendarNotFound("personal calendar not found") + output = [] + for item in client.Events( + calendar.uid, ["nmap.document", "nmap.event.calendars"], + start=start, end=end): + document = item.props.get("nmap.document") + if document is None: + continue + payload = to_ical(document.strip()) + filename = item.filename + if not filename.endswith(".ics"): + filename += ".ics" + if not EVENT_NAME.fullmatch(filename): + continue + output.append({"href": event_href(user, filename), + "etag": make_etag(payload), "data": payload}) + return output + except CalendarNotFound: + raise + except Exception as error: + raise RuntimeError("could not list calendar events") from error + finally: + _close(client) diff --git a/src/www/bongo_web/card_store.py b/src/www/bongo_web/card_store.py new file mode 100644 index 0000000..5ea7e6e --- /dev/null +++ b/src/www/bongo_web/card_store.py @@ -0,0 +1,222 @@ +"""CardDAV-to-Bongo Store mapping and lossless vCard storage.""" + +import hashlib +import json +import re +from urllib.parse import quote, unquote + + +CARD_NAME = re.compile(r"^[A-Za-z0-9._~-]{1,240}\.vcf$") +CONTACT_DOCUMENT_TYPE = 0x0004 +RAW_VCARD_KEY = "bongo.carddav.vcard" + + +class CardPathError(ValueError): + pass + + +class CardNotFound(LookupError): + pass + + +class CardPreconditionFailed(RuntimeError): + pass + + +def parse_card_path(path, authenticated_user): + parts = path.strip("/").split("/") + if len(parts) not in (3, 4) or parts[0] != "addressbooks": + raise CardPathError("not an address-book resource") + user = unquote(parts[1]) + if user != authenticated_user: + raise PermissionError("address-book owner differs from authenticated user") + if unquote(parts[2]) != "personal": + raise CardNotFound("address book does not exist") + if len(parts) == 3: + return {"kind": "addressbook", "user": user, + "addressbook": "personal"} + card = unquote(parts[3]) + if not CARD_NAME.fullmatch(card): + raise CardPathError("invalid card resource name") + return {"kind": "card", "user": user, "addressbook": "personal", + "card": card, "store_path": "/addressbook/personal/" + card[:-4]} + + +def card_href(user, card): + return "/addressbooks/{}/personal/{}".format( + quote(user, safe="@.-_~"), quote(card, safe="._~-")) + + +def _vobject(): + try: + import vobject + except ImportError as error: + raise RuntimeError("the system vobject package is required") from error + return vobject + + +def vcard_to_json(payload): + if len(payload) > 1048576: + raise ValueError("vCard is too large") + text = payload.decode("utf-8", "strict") + upper = text.upper() + if (upper.count("BEGIN:VCARD") != 1 + or upper.count("END:VCARD") != 1 + or "\nUID" not in upper.replace("\r\n", "\n") + or "\nFN" not in upper.replace("\r\n", "\n")): + raise ValueError("exactly one vCard with UID and FN is required") + card = _vobject().readOne(text) + if card.name.upper() != "VCARD" or not card.validate(): + raise ValueError("invalid vCard") + data = {RAW_VCARD_KEY: card.serialize(), "fn": card.fn.value, + "uid": card.uid.value} + emails = [] + for line in card.contents.get("email", []): + entry = {"value": str(line.value)} + for key, values in line.params.items(): + entry[key.lower()] = list(values) + emails.append(entry) + if emails: + data["email"] = emails + return json.dumps(data, ensure_ascii=False, separators=(",", ":")) + + +def json_to_vcard(document, fallback_uid="contact"): + data = json.loads(document.strip()) + raw = data.get(RAW_VCARD_KEY) + if raw: + return raw.encode("utf-8") + vobject = _vobject() + card = vobject.vCard() + card.add("uid").value = str(data.get("uid", fallback_uid)) + card.add("fn").value = str(data.get("fn") or data.get("fullName") + or fallback_uid) + for entry in data.get("email", []): + value = entry.get("value") or entry.get("address") + if not value: + continue + line = card.add("email") + line.value = str(value) + for key, values in entry.items(): + if key not in ("value", "address"): + line.params[key.upper()] = (values if isinstance(values, list) + else [str(values)]) + return card.serialize().encode("utf-8") + + +def make_etag(payload): + return '"{}"'.format(hashlib.sha256(payload).hexdigest()) + + +def open_store(user, store_cookie): + from bongo.store.StoreClient import StoreClient + return StoreClient(user, user, authCookie=store_cookie) + + +def etag_matches(condition, current): + if not condition: + return False + return condition.strip() == "*" or current in { + item.strip() for item in condition.split(",") + } + + +def _close(client): + if client is not None: + try: + client.Quit() + except Exception: + pass + + +def read_card(user, store_cookie, card, client_factory=open_store, + converter=json_to_vcard): + client = None + try: + client = client_factory(user, store_cookie) + path = "/addressbook/personal/" + card[:-4] + if client.Info(path) is None: + raise CardNotFound("card not found") + payload = converter(client.Read(path), card[:-4]) + return payload, make_etag(payload) + except CardNotFound: + raise + except Exception as error: + raise RuntimeError("could not read card") from error + finally: + _close(client) + + +def write_card(user, store_cookie, card, payload, if_match=None, + if_none_match=None, client_factory=open_store, + to_json=vcard_to_json, to_vcard=json_to_vcard): + document = to_json(payload) + path = "/addressbook/personal/" + card[:-4] + client = None + try: + client = client_factory(user, store_cookie) + exists = client.Info(path) is not None + current_etag = None + if exists and (if_match or if_none_match): + current_etag = make_etag(to_vcard(client.Read(path), card[:-4])) + if if_none_match and exists and etag_matches(if_none_match, current_etag): + raise CardPreconditionFailed("card already exists") + if if_match and (not exists or not etag_matches(if_match, current_etag)): + raise CardPreconditionFailed("card changed") + if exists: + client.Replace(path, document) + else: + client.Write("/addressbook/personal", CONTACT_DOCUMENT_TYPE, + document, filename=card[:-4]) + return not exists, make_etag(to_vcard(document, card[:-4])) + except (CardPreconditionFailed, ValueError): + raise + except Exception as error: + raise RuntimeError("could not store card") from error + finally: + _close(client) + + +def delete_card(user, store_cookie, card, if_match=None, + client_factory=open_store, converter=json_to_vcard): + path = "/addressbook/personal/" + card[:-4] + client = None + try: + client = client_factory(user, store_cookie) + if client.Info(path) is None: + raise CardNotFound("card not found") + if if_match: + current = make_etag(converter(client.Read(path), card[:-4])) + if not etag_matches(if_match, current): + raise CardPreconditionFailed("card changed") + client.Delete(path) + except (CardNotFound, CardPreconditionFailed): + raise + except Exception as error: + raise RuntimeError("could not delete card") from error + finally: + _close(client) + + +def list_cards(user, store_cookie, client_factory=open_store, + converter=json_to_vcard): + client = None + try: + client = client_factory(user, store_cookie) + output = [] + for item in client.List("/addressbook/personal", ["nmap.document"]): + document = item.props.get("nmap.document") + if document is None: + continue + filename = item.filename.rsplit("/", 1)[-1] + card = filename if filename.endswith(".vcf") else filename + ".vcf" + if not CARD_NAME.fullmatch(card): + continue + payload = converter(document, filename) + output.append({"href": card_href(user, card), + "etag": make_etag(payload), "data": payload}) + return output + except Exception as error: + raise RuntimeError("could not list cards") from error + finally: + _close(client) diff --git a/src/www/bongo_web/compose.py b/src/www/bongo_web/compose.py new file mode 100644 index 0000000..b3b7cfe --- /dev/null +++ b/src/www/bongo_web/compose.py @@ -0,0 +1,107 @@ +"""Build and submit messages created by the webmail composer.""" + +from __future__ import annotations + +from email.headerregistry import Address +from email.message import EmailMessage +from email.policy import SMTP +from email.utils import getaddresses + + +MAX_RECIPIENTS = 100 +MAX_SUBJECT = 998 +MAX_BODY = 1024 * 1024 + + +def _address(value: str) -> str: + if not value or any(character in value for character in "\r\n\0"): + raise ValueError("invalid email address") + try: + parsed = Address(addr_spec=value) + except (TypeError, ValueError) as error: + raise ValueError("invalid email address") from error + if not parsed.username or not parsed.domain: + raise ValueError("invalid email address") + domain = parsed.domain.encode("idna").decode("ascii") + return str(Address(username=parsed.username, domain=domain)) + + +def _addresses(value) -> list[tuple[str, str]]: + if value is None: + return [] + if isinstance(value, list): + raw = [str(item) for item in value] + elif isinstance(value, str): + raw = [value] + else: + raise ValueError("address list must be text or a list") + if any(any(character in item for character in "\r\n\0") for item in raw): + raise ValueError("invalid email address") + parsed = getaddresses(raw) + if not parsed or any(not address for _name, address in parsed): + raise ValueError("invalid email address") + result = [] + for name, address in parsed: + result.append((name.strip(), _address(address))) + return result + + +def build_message(sender: str, data: dict) -> tuple[EmailMessage, list[str]]: + """Return a validated RFC message and its envelope recipients.""" + sender = _address(sender) + recipients = {header: _addresses(data.get(header.casefold())) + for header in ("To", "Cc", "Bcc")} + envelope = [address for values in recipients.values() + for _name, address in values] + if not envelope: + raise ValueError("at least one recipient is required") + if len(envelope) > MAX_RECIPIENTS: + raise ValueError("too many recipients") + + subject = str(data.get("subject", "")) + body = str(data.get("text", "")) + if any(character in subject for character in "\r\n\0"): + raise ValueError("invalid subject") + if len(subject) > MAX_SUBJECT or len(body.encode("utf-8")) > MAX_BODY: + raise ValueError("message is too large") + + message = EmailMessage(policy=SMTP) + message["From"] = sender + for header in ("To", "Cc"): + if recipients[header]: + message[header] = [Address(display_name=name, addr_spec=address) + for name, address in recipients[header]] + message["Subject"] = subject + message.set_content(body) + return message, envelope + + +def send_message(sender: str, data: dict, queue_factory=None) -> None: + """Submit a composed message to Bongo's native queue service.""" + message, recipients = build_message(sender, data) + if queue_factory is None: + from bongo.store.QueueClient import QueueClient + queue_factory = QueueClient + queue = queue_factory(sender) + created = False + try: + queue.Create() + created = True + queue.StoreFrom(_address(sender), _address(sender)) + for recipient in recipients: + queue.StoreTo(recipient, recipient) + # SMTP policy emits a seven/eight-bit clean wire representation with CRLF. + queue.StoreMessage(message.as_string(policy=SMTP)) + queue.Run() + except Exception: + if created: + try: + queue.Abort() + except Exception: + pass + raise + finally: + try: + queue.Quit() + except Exception: + pass diff --git a/src/www/bongo_web/dav.py b/src/www/bongo_web/dav.py new file mode 100644 index 0000000..9d365ba --- /dev/null +++ b/src/www/bongo_web/dav.py @@ -0,0 +1,281 @@ +"""Small, dependency-free DAV discovery helpers. + +This module deliberately implements only the discovery surface advertised by +the server. Calendar object reads and writes are added separately once their +Store mapping has been verified. +""" + +from datetime import datetime, timezone +from urllib.parse import quote +from xml.etree import ElementTree as ET + + +DAV = "DAV:" +CALDAV = "urn:ietf:params:xml:ns:caldav" +CARDDAV = "urn:ietf:params:xml:ns:carddav" +CS = "http://calendarserver.org/ns/" + +ET.register_namespace("D", DAV) +ET.register_namespace("C", CALDAV) +ET.register_namespace("A", CARDDAV) +ET.register_namespace("CS", CS) + + +def qname(namespace, name): + return f"{{{namespace}}}{name}" + + +def requested_properties(body): + """Return requested Clark names, or None for DAV:allprop/empty bodies.""" + if not body or not body.strip(): + return None + if len(body) > 65536: + raise ValueError("PROPFIND document is too large") + root = ET.fromstring(body) + if root.tag != qname(DAV, "propfind"): + raise ValueError("DAV:propfind required") + prop = root.find(qname(DAV, "prop")) + if prop is None: + return None + return [child.tag for child in prop] + + +def _parse_utc(value): + if not value: + return None + try: + return datetime.strptime(value, "%Y%m%dT%H%M%SZ").replace( + tzinfo=timezone.utc) + except ValueError as error: + raise ValueError("invalid CalDAV time-range") from error + + +def parse_report(body): + if not body or len(body) > 65536: + raise ValueError("invalid REPORT document") + root = ET.fromstring(body) + query = qname(CALDAV, "calendar-query") + multiget = qname(CALDAV, "calendar-multiget") + if root.tag not in (query, multiget): + raise ValueError("unsupported calendar REPORT") + prop = root.find(qname(DAV, "prop")) + properties = [child.tag for child in prop] if prop is not None else [] + result = {"kind": "query" if root.tag == query else "multiget", + "properties": properties, "start": None, "end": None, + "hrefs": []} + if root.tag == query: + time_range = root.find(f".//{qname(CALDAV, 'time-range')}") + if time_range is not None: + result["start"] = _parse_utc(time_range.get("start")) + result["end"] = _parse_utc(time_range.get("end")) + if bool(result["start"]) != bool(result["end"]): + raise ValueError("both time-range bounds are required") + else: + result["hrefs"] = [element.text for element in + root.findall(qname(DAV, "href")) if element.text] + if len(result["hrefs"]) > 1000: + raise ValueError("too many calendar resources requested") + return result + + +def parse_card_report(body): + if not body or len(body) > 65536: + raise ValueError("invalid REPORT document") + root = ET.fromstring(body) + query = qname(CARDDAV, "addressbook-query") + multiget = qname(CARDDAV, "addressbook-multiget") + if root.tag not in (query, multiget): + raise ValueError("unsupported address-book REPORT") + prop = root.find(qname(DAV, "prop")) + properties = [child.tag for child in prop] if prop is not None else [] + hrefs = [] + if root.tag == multiget: + hrefs = [element.text for element in root.findall(qname(DAV, "href")) + if element.text] + if len(hrefs) > 1000: + raise ValueError("too many address-book resources requested") + return {"kind": "query" if root.tag == query else "multiget", + "properties": properties, "hrefs": hrefs} + + +def event_multistatus(events, properties, requested_hrefs=None): + by_href = {event["href"]: event for event in events} + hrefs = requested_hrefs if requested_hrefs is not None else list(by_href) + root = ET.Element(qname(DAV, "multistatus")) + for href in hrefs: + response = ET.SubElement(root, qname(DAV, "response")) + ET.SubElement(response, qname(DAV, "href")).text = href + event = by_href.get(href) + if event is None: + ET.SubElement(response, qname(DAV, "status")).text = \ + "HTTP/1.1 404 Not Found" + continue + propstat = ET.SubElement(response, qname(DAV, "propstat")) + prop = ET.SubElement(propstat, qname(DAV, "prop")) + for name in properties: + if name == qname(DAV, "getetag"): + ET.SubElement(prop, name).text = event["etag"] + elif name == qname(CALDAV, "calendar-data"): + data = event["data"] + ET.SubElement(prop, name).text = data.decode("utf-8") + ET.SubElement(propstat, qname(DAV, "status")).text = \ + "HTTP/1.1 200 OK" + return ET.tostring(root, encoding="utf-8", xml_declaration=True) + + +def card_multistatus(cards, properties, requested_hrefs=None): + by_href = {card["href"]: card for card in cards} + hrefs = requested_hrefs if requested_hrefs is not None else list(by_href) + root = ET.Element(qname(DAV, "multistatus")) + for href in hrefs: + response = ET.SubElement(root, qname(DAV, "response")) + ET.SubElement(response, qname(DAV, "href")).text = href + card = by_href.get(href) + if card is None: + ET.SubElement(response, qname(DAV, "status")).text = \ + "HTTP/1.1 404 Not Found" + continue + propstat = ET.SubElement(response, qname(DAV, "propstat")) + prop = ET.SubElement(propstat, qname(DAV, "prop")) + for name in properties: + if name == qname(DAV, "getetag"): + ET.SubElement(prop, name).text = card["etag"] + elif name == qname(CARDDAV, "address-data"): + ET.SubElement(prop, name).text = card["data"].decode("utf-8") + ET.SubElement(propstat, qname(DAV, "status")).text = \ + "HTTP/1.1 200 OK" + return ET.tostring(root, encoding="utf-8", xml_declaration=True) + + +def _resource_type(*types): + element = ET.Element(qname(DAV, "resourcetype")) + for namespace, name in types: + ET.SubElement(element, qname(namespace, name)) + return element + + +def _href_property(name, href, namespace=DAV): + element = ET.Element(qname(namespace, name)) + ET.SubElement(element, qname(DAV, "href")).text = href + return element + + +def discovery_resources(user): + encoded = quote(user, safe="@.-_~") + principal = f"/principals/{encoded}/" + home = f"/calendars/{encoded}/" + calendar = f"{home}personal/" + address_home = f"/addressbooks/{encoded}/" + addressbook = f"{address_home}personal/" + return { + "/calendars/": { + qname(DAV, "resourcetype"): _resource_type((DAV, "collection")), + qname(DAV, "displayname"): "Bongo calendars", + qname(DAV, "current-user-principal"): _href_property( + "current-user-principal", principal), + }, + "/principals/": { + qname(DAV, "resourcetype"): _resource_type((DAV, "collection")), + qname(DAV, "displayname"): "Bongo principals", + qname(DAV, "current-user-principal"): _href_property( + "current-user-principal", principal), + }, + principal: { + qname(DAV, "resourcetype"): _resource_type((DAV, "principal")), + qname(DAV, "displayname"): user, + qname(DAV, "current-user-principal"): _href_property( + "current-user-principal", principal), + qname(CALDAV, "calendar-home-set"): _href_property( + "calendar-home-set", home, CALDAV), + qname(CARDDAV, "addressbook-home-set"): _href_property( + "addressbook-home-set", address_home, CARDDAV), + }, + home: { + qname(DAV, "resourcetype"): _resource_type((DAV, "collection")), + qname(DAV, "displayname"): f"Calendars for {user}", + qname(DAV, "owner"): _href_property("owner", principal), + }, + calendar: { + qname(DAV, "resourcetype"): _resource_type( + (DAV, "collection"), (CALDAV, "calendar")), + qname(DAV, "displayname"): "Personal", + qname(DAV, "owner"): _href_property("owner", principal), + qname(CALDAV, "supported-calendar-component-set"): + _component_set(), + qname(CS, "getctag"): "0", + }, + "/addressbooks/": { + qname(DAV, "resourcetype"): _resource_type((DAV, "collection")), + qname(DAV, "displayname"): "Bongo address books", + qname(DAV, "current-user-principal"): _href_property( + "current-user-principal", principal), + }, + address_home: { + qname(DAV, "resourcetype"): _resource_type((DAV, "collection")), + qname(DAV, "displayname"): f"Address books for {user}", + qname(DAV, "owner"): _href_property("owner", principal), + }, + addressbook: { + qname(DAV, "resourcetype"): _resource_type( + (DAV, "collection"), (CARDDAV, "addressbook")), + qname(DAV, "displayname"): "Personal", + qname(DAV, "owner"): _href_property("owner", principal), + qname(CS, "getctag"): "0", + }, + } + + +def _component_set(): + element = ET.Element(qname(CALDAV, "supported-calendar-component-set")) + ET.SubElement(element, qname(CALDAV, "comp"), {"name": "VEVENT"}) + return element + + +def _append_property(parent, name, value): + if isinstance(value, ET.Element): + parent.append(ET.fromstring(ET.tostring(value))) + else: + ET.SubElement(parent, name).text = value + + +def multistatus(path, user, depth, requested=None): + resources = discovery_resources(user) + if path not in resources: + return None + selected = [path] + if depth == 1: + selected.extend(candidate for candidate in resources + if candidate != path + and candidate.startswith(path) + and "/" not in candidate[len(path):].rstrip("/")) + root = ET.Element(qname(DAV, "multistatus")) + for href in selected: + response = ET.SubElement(root, qname(DAV, "response")) + ET.SubElement(response, qname(DAV, "href")).text = href + available = resources[href] + wanted = list(available) if requested is None else requested + found = [name for name in wanted if name in available] + missing = [name for name in wanted if name not in available] + if found: + propstat = ET.SubElement(response, qname(DAV, "propstat")) + prop = ET.SubElement(propstat, qname(DAV, "prop")) + for name in found: + _append_property(prop, name, available[name]) + ET.SubElement(propstat, qname(DAV, "status")).text = \ + "HTTP/1.1 200 OK" + if missing: + propstat = ET.SubElement(response, qname(DAV, "propstat")) + prop = ET.SubElement(propstat, qname(DAV, "prop")) + for name in missing: + ET.SubElement(prop, name) + ET.SubElement(propstat, qname(DAV, "status")).text = \ + "HTTP/1.1 404 Not Found" + return ET.tostring(root, encoding="utf-8", xml_declaration=True) + + +def options_headers(): + return { + "Allow": "OPTIONS, GET, HEAD, PUT, DELETE, PROPFIND, REPORT", + "DAV": "1, 3, access-control, calendar-access, addressbook", + "MS-Author-Via": "DAV", + } diff --git a/src/www/bongo_web/filters.py b/src/www/bongo_web/filters.py new file mode 100644 index 0000000..8d23f28 --- /dev/null +++ b/src/www/bongo_web/filters.py @@ -0,0 +1,133 @@ +"""Validated user filters and compilation to Bongo's legacy rule format.""" + +import re + + +ADDRESS = re.compile(r"^[^\s@<>]+@[^\s@<>]+$") +DOMAIN = re.compile(r"^(?:[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?\.)+[A-Za-z]{2,63}$") +LIST_ID = re.compile(r"^[A-Za-z0-9_.-]+$") +FOLDER = re.compile(r"^/mail/[^\x00]+$") + +CONDITION_CODES = { + "sender": "B", "recipient": "C", "subject": "D", + "header": "F", "size_more": "N", "size_less": "O", + "mailing_list": "h", "list_id": "i", +} +ACTION_CODES = {"delete": "B", "forward": "C", "copy": "D", + "move": "E", "stop": "F"} + + +def validate_filter(value): + if not isinstance(value, dict): + raise ValueError("filter must be an object") + conditions = value.get("conditions") + actions = value.get("actions") + if not isinstance(conditions, list) or not conditions: + raise ValueError("at least one condition is required") + if not isinstance(actions, list) or not actions: + raise ValueError("at least one action is required") + if len(conditions) > 100 or len(actions) > 20: + raise ValueError("filter is too complex") + join = value.get("match", "all") + if join not in {"all", "any"}: + raise ValueError("invalid condition operator") + for condition in conditions: + _validate_condition(condition) + for action in actions: + _validate_action(action) + return value + + +def _text(value, maximum=998): + value = str(value) + if not value or len(value.encode("utf-8")) > maximum or "\x00" in value: + raise ValueError("invalid filter value") + return value + + +def _validate_condition(condition): + if not isinstance(condition, dict): + raise ValueError("condition must be an object") + kind = condition.get("type") + if kind == "mailing_list": + return + if kind in {"sender", "recipient"}: + value = _text(condition.get("value", ""), 320) + if "@" in value and not ADDRESS.fullmatch(value): + raise ValueError("invalid mail address") + elif kind == "sender_domain": + if not DOMAIN.fullmatch(_text(condition.get("value", ""), 253)): + raise ValueError("invalid sender domain") + elif kind == "contact_group": + _text(condition.get("value", ""), 255) + elif kind == "list_id": + if not LIST_ID.fullmatch(_text(condition.get("value", ""), 255)): + raise ValueError("invalid List-ID") + elif kind in {"subject", "size_more", "size_less"}: + _text(condition.get("value", "")) + elif kind == "header": + name = _text(condition.get("name", ""), 78) + if not re.fullmatch(r"[!-9;-~]+", name): + raise ValueError("invalid header name") + _text(condition.get("value", "")) + else: + raise ValueError("unknown filter condition") + + +def _validate_action(action): + if not isinstance(action, dict): + raise ValueError("action must be an object") + kind = action.get("type") + if kind in {"move", "copy"}: + folder = _text(action.get("folder", ""), 999) + if not FOLDER.fullmatch(folder) or any(part in {"", ".", ".."} + for part in folder.split("/")[2:]): + raise ValueError("invalid destination folder") + elif kind == "forward": + if not ADDRESS.fullmatch(_text(action.get("address", ""), 320)): + raise ValueError("invalid forwarding address") + elif kind not in {"delete", "stop"}: + raise ValueError("unknown filter action") + + +def _argument(code, first="", second=""): + first = first.encode("utf-8") + second = second.encode("utf-8") + if len(first) > 999 or len(second) > 999: + raise ValueError("compiled rule argument is too long") + return (code.encode("ascii") + f"{len(first):03d}".encode("ascii") + first + + b"Z" + f"{len(second):03d}".encode("ascii") + second + b"Z") + + +def compile_filter(value, rule_id, group_resolver=lambda _name: ()): + """Compile a validated filter; contact groups expand to sender OR terms.""" + validate_filter(value) + compiled = [] + for condition in value["conditions"]: + kind = condition["type"] + if kind == "contact_group": + addresses = sorted(set(group_resolver(condition["value"]))) + if not addresses or any(not ADDRESS.fullmatch(address) + for address in addresses): + raise ValueError("contact group has no valid addresses") + compiled.extend(("B", address, "") for address in addresses) + elif kind == "sender_domain": + compiled.append(("B", "@" + condition["value"].casefold(), "")) + elif kind == "header": + compiled.append(("F", condition["name"], condition["value"])) + elif kind == "mailing_list": + compiled.append(("h", "", "")) + else: + compiled.append((CONDITION_CODES[kind], str(condition["value"]), "")) + join = b"X" if value.get("match", "all") == "all" else b"Y" + output = f"{int(rule_id):08X}".encode("ascii") + b"A" + for index, condition in enumerate(compiled): + output += _argument(*condition) + if index + 1 < len(compiled): + output += join + for action in value["actions"]: + kind = action["type"] + argument = (action.get("folder", "")[6:] if kind in {"move", "copy"} + else action.get("address", "") if kind == "forward" else "") + output += _argument(ACTION_CODES[kind], argument) + return output.decode("utf-8") diff --git a/src/www/bongo_web/html_mail.py b/src/www/bongo_web/html_mail.py new file mode 100644 index 0000000..afd4f16 --- /dev/null +++ b/src/www/bongo_web/html_mail.py @@ -0,0 +1,71 @@ +"""Sanitize untrusted HTML mail while preserving safe inline images.""" + +import re +from urllib.parse import urlsplit + + +ALLOWED_TAGS = { + "a", "abbr", "b", "blockquote", "br", "code", "del", "div", "em", + "h1", "h2", "h3", "h4", "h5", "h6", "hr", "i", "img", "ins", + "li", "ol", "p", "pre", "s", "span", "strong", "sub", "sup", + "table", "tbody", "td", "tfoot", "th", "thead", "tr", "u", "ul", +} +ALLOWED_ATTRIBUTES = { + "a": {"href", "title"}, + "img": {"src", "alt", "title", "width", "height"}, + "td": {"colspan", "rowspan", "style"}, + "th": {"colspan", "rowspan", "style"}, + "*": {"style"}, +} +ALLOWED_STYLE_PROPERTIES = { + "background-color", "border", "border-color", "border-style", + "border-width", "color", "font-family", "font-size", "font-style", + "font-weight", "margin", "margin-bottom", "margin-left", "margin-right", + "margin-top", "padding", "padding-bottom", "padding-left", + "padding-right", "padding-top", "text-align", "text-decoration", + "vertical-align", "white-space", +} +DATA_IMAGE = re.compile( + r"^data:image/(?:png|jpeg|gif|webp);base64,[A-Za-z0-9+/=\r\n]+$", + re.IGNORECASE) +MAX_DATA_IMAGE_URL = 350000 + + +def sanitize_html(html, cleaner=None): + """Return sanitized HTML and remote image URLs in placeholder order.""" + if not isinstance(html, str) or len(html.encode("utf-8")) > 4 * 1024 * 1024: + raise ValueError("HTML mail part is too large") + remote_images = [] + + def filter_attribute(tag, attribute, value): + if attribute == "src" and tag == "img": + scheme = urlsplit(value).scheme.casefold() + if scheme in ("http", "https"): + remote_images.append(value) + return "bongo-remote:{}".format(len(remote_images) - 1) + if scheme == "cid": + return value + if scheme == "data" and len(value) <= MAX_DATA_IMAGE_URL \ + and DATA_IMAGE.fullmatch(value): + return value + return None + if attribute == "href" and tag == "a": + return value if urlsplit(value).scheme.casefold() in ( + "http", "https", "mailto") else None + return value + + if cleaner is None: + try: + import nh3 + except ImportError as error: + raise RuntimeError("the system nh3 package is required") from error + cleaner = nh3.clean + sanitized = cleaner( + html, tags=ALLOWED_TAGS, + clean_content_tags={"script", "style", "template", "iframe", "object", + "embed", "form", "svg", "math", "meta", "link"}, + attributes=ALLOWED_ATTRIBUTES, attribute_filter=filter_attribute, + link_rel="noopener noreferrer nofollow", + url_schemes={"http", "https", "mailto", "cid", "data", "bongo-remote"}, + filter_style_properties=ALLOWED_STYLE_PROPERTIES) + return {"html": sanitized, "remote_images": remote_images} diff --git a/src/www/bongo_web/mail_store.py b/src/www/bongo_web/mail_store.py new file mode 100644 index 0000000..aa6f5fe --- /dev/null +++ b/src/www/bongo_web/mail_store.py @@ -0,0 +1,199 @@ +"""Read-only webmail views backed by the Bongo Store.""" + +import base64 +import re +from email import policy +from email.parser import BytesParser +from email.utils import getaddresses + +from .html_mail import sanitize_html + + +CONVERSATION_PROPERTIES = [ + "bongo.from", "nmap.conversation.count", "nmap.conversation.date", + "nmap.conversation.subject", "bongo.conversation.subject", + "nmap.conversation.unread", +] +DOCUMENT_ID = re.compile(r"^[0-9A-Fa-f]{1,64}$") +INLINE_IMAGE_TYPES = {"image/png", "image/jpeg", "image/gif", "image/webp"} +LIST_ID = re.compile(r"<\s*([^<>\s]+)\s*>") + + +def open_store(user, store_cookie): + from bongo.store.StoreClient import StoreClient + return StoreClient(user, user, authCookie=store_cookie) + + +def _close(client): + if client is not None: + try: + client.Quit() + except Exception: + pass + + +def _folder_path(name): + name = str(name) + if not name.startswith("/mail/") or "\x00" in name: + raise ValueError("invalid mail folder") + components = name.split("/")[2:] + if not components or any(part in ("", ".", "..") for part in components): + raise ValueError("invalid mail folder") + return name + + +def list_folders(user, store_cookie, client_factory=open_store): + client = None + try: + client = client_factory(user, store_cookie) + folders = [] + for collection in client.Collections("/mail"): + path = collection.name + if not path.startswith("/mail/"): + path = "/mail/" + path.lstrip("/") + try: + path = _folder_path(path) + except ValueError: + continue + folders.append({"path": path, "name": path.rsplit("/", 1)[-1]}) + return sorted(folders, key=lambda item: (item["name"].upper() != "INBOX", + item["name"].casefold())) + except Exception as error: + raise RuntimeError("could not list mail folders") from error + finally: + _close(client) + + +def _addresses(value): + if not value: + return [] + return [{"name": name, "address": address} + for name, address in getaddresses(value.splitlines()) if address] + + +def list_conversations(user, store_cookie, folder, offset=0, limit=30, + client_factory=open_store): + folder = _folder_path(folder) + offset = int(offset) + limit = int(limit) + if offset < 0 or limit < 1 or limit > 100: + raise ValueError("invalid result range") + client = None + try: + client = client_factory(user, store_cookie) + known = {item["path"] for item in list_folders( + user, store_cookie, client_factory=lambda _u, _c: _Borrowed(client))} + if folder not in known: + raise FileNotFoundError("mail folder not found") + iterator = client.Conversations(folder, CONVERSATION_PROPERTIES, + start=offset, end=offset + limit - 1, + showTotal=True) + conversations = [] + for item in iterator: + props = item.props + conversations.append({ + "id": str(item.uid), + "from": _addresses(props.get("bongo.from", "")), + "subject": props.get("bongo.conversation.subject") + or props.get("nmap.conversation.subject", ""), + "date": props.get("nmap.conversation.date", ""), + "unread": int(props.get("nmap.conversation.unread", 0)), + "count": int(props.get("nmap.conversation.count", 1)), + "flags": int(item.flags), + }) + return {"folder": folder, "offset": offset, "limit": limit, + "total": int(getattr(iterator, "total", len(conversations))), + "conversations": conversations} + except (ValueError, FileNotFoundError): + raise + except Exception as error: + raise RuntimeError("could not list conversations") from error + finally: + _close(client) + + +def _message_content(raw, sanitizer=sanitize_html): + if isinstance(raw, str): + raw = raw.encode("utf-8", "surrogateescape") + if len(raw) > 25 * 1024 * 1024: + raise ValueError("message is too large for web display") + message = BytesParser(policy=policy.default).parsebytes(raw) + plain = None + html = None + inline_images = {} + parts = message.walk() if message.is_multipart() else (message,) + total_inline = 0 + for part in parts: + if part.is_multipart(): + continue + content_type = part.get_content_type().casefold() + disposition = part.get_content_disposition() + if content_type == "text/plain" and disposition != "attachment" and plain is None: + plain = part.get_content() + elif content_type == "text/html" and disposition != "attachment" and html is None: + html = part.get_content() + elif content_type in INLINE_IMAGE_TYPES: + content_id = (part.get("Content-ID") or "").strip("<> ") + payload = part.get_payload(decode=True) or b"" + total_inline += len(payload) + if content_id and len(payload) <= 2 * 1024 * 1024 \ + and total_inline <= 5 * 1024 * 1024: + inline_images[content_id] = "data:{};base64,{}".format( + content_type, base64.b64encode(payload).decode("ascii")) + sanitized = sanitizer(html) if html is not None else None + list_id_header = str(message.get("List-ID", "")) + list_id_match = LIST_ID.search(list_id_header) + list_id = list_id_match.group(1).casefold() if list_id_match else None + list_post = str(message.get("List-Post", "")) + return { + "from": _addresses(str(message.get("From", ""))), + "to": _addresses(str(message.get("To", ""))), + "cc": _addresses(str(message.get("Cc", ""))), + "subject": str(message.get("Subject", "")), + "date": str(message.get("Date", "")), + "message_id": str(message.get("Message-ID", "")), + "text": plain, + "html": sanitized["html"] if sanitized else None, + "remote_images": sanitized["remote_images"] if sanitized else [], + "inline_images": inline_images, + "mailing_list": ({"id": list_id, "post": list_post} + if list_id else None), + } + + +def read_conversation(user, store_cookie, conversation_id, + client_factory=open_store, sanitizer=sanitize_html): + if not DOCUMENT_ID.fullmatch(str(conversation_id)): + raise ValueError("invalid conversation id") + client = None + try: + client = client_factory(user, store_cookie) + output = [] + for item in client.Conversation(conversation_id): + if int(item.type) != 0x0002: + continue + content = _message_content(client.Read(item.uid), sanitizer) + content["id"] = str(item.uid) + content["flags"] = int(item.flags) + output.append(content) + if not output: + raise FileNotFoundError("conversation not found") + return {"id": conversation_id, "messages": output} + except (ValueError, FileNotFoundError): + raise + except Exception as error: + raise RuntimeError("could not read conversation") from error + finally: + _close(client) + + +class _Borrowed: + """Prevent the folder helper from closing a caller-owned Store client.""" + def __init__(self, client): + self.client = client + + def __getattr__(self, name): + return getattr(self.client, name) + + def Quit(self): + pass diff --git a/src/www/bongo_web/session.py b/src/www/bongo_web/session.py new file mode 100644 index 0000000..4b98b5c --- /dev/null +++ b/src/www/bongo_web/session.py @@ -0,0 +1,89 @@ +"""Opaque, server-side web sessions. + +Only a hash of the browser token is stored. The Bongo Store cookie remains +server-side and the user's password is never retained after login. +""" + +import hashlib +import os +import secrets +import sqlite3 +import time + + +COOKIE_NAME = "__Host-bongo-session" + + +class SessionStore: + def __init__(self, path, lifetime=3600): + self.path = path + self.lifetime = int(lifetime) + directory = os.path.dirname(path) + os.makedirs(directory, mode=0o700, exist_ok=True) + os.chmod(directory, 0o700) + self._initialize() + + def _connect(self): + connection = sqlite3.connect(self.path, timeout=5) + connection.execute("PRAGMA busy_timeout=5000") + connection.execute("PRAGMA journal_mode=WAL") + connection.execute("PRAGMA foreign_keys=ON") + return connection + + def _initialize(self): + with self._connect() as database: + database.execute( + "CREATE TABLE IF NOT EXISTS sessions (" + "id_hash BLOB PRIMARY KEY, user TEXT NOT NULL, " + "store_cookie TEXT NOT NULL, csrf_token TEXT NOT NULL, " + "created INTEGER NOT NULL, expires INTEGER NOT NULL)") + database.execute( + "CREATE INDEX IF NOT EXISTS sessions_expiry ON sessions(expires)") + os.chmod(self.path, 0o600) + + @staticmethod + def _hash(token): + return hashlib.sha256(token.encode("ascii")).digest() + + def create(self, user, store_cookie): + token = secrets.token_urlsafe(32) + csrf_token = secrets.token_urlsafe(32) + now = int(time.time()) + with self._connect() as database: + database.execute("DELETE FROM sessions WHERE expires <= ?", (now,)) + database.execute( + "INSERT INTO sessions VALUES (?, ?, ?, ?, ?, ?)", + (self._hash(token), user, store_cookie, csrf_token, + now, now + self.lifetime)) + return token, csrf_token + + def get(self, token): + if not token: + return None + now = int(time.time()) + with self._connect() as database: + row = database.execute( + "SELECT user, store_cookie, csrf_token, expires " + "FROM sessions WHERE id_hash = ? AND expires > ?", + (self._hash(token), now)).fetchone() + if row is None: + return None + return {"user": row[0], "store_cookie": row[1], + "csrf_token": row[2], "expires": row[3]} + + def delete(self, token): + if not token: + return + with self._connect() as database: + database.execute("DELETE FROM sessions WHERE id_hash = ?", + (self._hash(token),)) + + +def session_cookie(token, max_age): + return (f"{COOKIE_NAME}={token}; Path=/; Max-Age={int(max_age)}; " + "Secure; HttpOnly; SameSite=Strict") + + +def expired_session_cookie(): + return (f"{COOKIE_NAME}=; Path=/; Max-Age=0; " + "Secure; HttpOnly; SameSite=Strict") diff --git a/src/www/bongo_web/tasks.py b/src/www/bongo_web/tasks.py new file mode 100644 index 0000000..1c6b70b --- /dev/null +++ b/src/www/bongo_web/tasks.py @@ -0,0 +1,94 @@ +"""Persistent, per-user decisions surfaced by the web UI.""" + +import json +import sqlite3 +import time + + +VALID_STATES = {"open", "ignored", "disabled", "completed"} +VALID_SEVERITIES = {"info", "warning", "critical"} + + +class TaskStore: + def __init__(self, path): + self.path = path + with self._connect() as database: + database.executescript(""" + CREATE TABLE IF NOT EXISTS user_tasks ( + id INTEGER PRIMARY KEY, + user TEXT NOT NULL, + task_type TEXT NOT NULL, + object_key TEXT NOT NULL, + severity TEXT NOT NULL DEFAULT 'info', + title TEXT NOT NULL, + details TEXT NOT NULL DEFAULT '{}', + state TEXT NOT NULL DEFAULT 'open', + dismissible INTEGER NOT NULL DEFAULT 1, + created_at INTEGER NOT NULL, + updated_at INTEGER NOT NULL, + UNIQUE(user, task_type, object_key) + ); + CREATE INDEX IF NOT EXISTS user_tasks_open + ON user_tasks(user, state, severity); + """) + + def _connect(self): + database = sqlite3.connect(self.path, timeout=5) + database.row_factory = sqlite3.Row + database.execute("PRAGMA journal_mode=WAL") + database.execute("PRAGMA busy_timeout=5000") + return database + + def upsert(self, user, task_type, object_key, title, details=None, + severity="info", dismissible=True): + if severity not in VALID_SEVERITIES: + raise ValueError("invalid task severity") + now = int(time.time()) + payload = json.dumps(details or {}, ensure_ascii=False, + separators=(",", ":")) + with self._connect() as database: + database.execute(""" + INSERT INTO user_tasks + (user, task_type, object_key, severity, title, details, + dismissible, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(user, task_type, object_key) DO UPDATE SET + severity=excluded.severity, title=excluded.title, + details=excluded.details, updated_at=excluded.updated_at + WHERE user_tasks.state NOT IN ('disabled', 'completed') + """, (user, task_type, object_key, severity, title, payload, + int(dismissible), now, now)) + + def list(self, user, include_closed=False): + clause = "" if include_closed else " AND state = 'open'" + with self._connect() as database: + rows = database.execute( + "SELECT * FROM user_tasks WHERE user = ?" + clause + + " ORDER BY CASE severity WHEN 'critical' THEN 0 " + "WHEN 'warning' THEN 1 ELSE 2 END, updated_at DESC", (user,)) + return [self._row(row) for row in rows] + + def set_state(self, user, task_id, state): + if state not in VALID_STATES: + raise ValueError("invalid task state") + with self._connect() as database: + row = database.execute( + "SELECT severity, dismissible FROM user_tasks " + "WHERE id = ? AND user = ?", (int(task_id), user)).fetchone() + if row is None: + raise FileNotFoundError("task not found") + if state in {"ignored", "disabled"} and (not row["dismissible"] + or row["severity"] == "critical"): + raise PermissionError("task cannot be dismissed") + database.execute( + "UPDATE user_tasks SET state = ?, updated_at = ? " + "WHERE id = ? AND user = ?", + (state, int(time.time()), int(task_id), user)) + + @staticmethod + def _row(row): + return {"id": row["id"], "type": row["task_type"], + "key": row["object_key"], "severity": row["severity"], + "title": row["title"], "details": json.loads(row["details"]), + "state": row["state"], "dismissible": bool(row["dismissible"]), + "created_at": row["created_at"], "updated_at": row["updated_at"]} diff --git a/src/www/static/Bongo-logo.png b/src/www/static/Bongo-logo.png new file mode 100755 index 0000000000000000000000000000000000000000..a752adc3986cb85a0812119b500a1eb7a70ccad5 GIT binary patch literal 5439 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 literal 0 HcmV?d00001 diff --git a/src/www/static/agent-control.png b/src/www/static/agent-control.png new file mode 100644 index 0000000000000000000000000000000000000000..b631d869d7122b9ccceccb14a0c38d85676122b7 GIT binary patch literal 3386 zcmV-A4aM?_P)kwc;aAC$_c1LS#5@A+%M>2vVZUD)v zlTNzx?7TXCZ@=z6`$y7lCM1xDRn}JdtDL&2)a~Ez^Etmh_ny8T!a2wP=McVbfT_ke z0$+E0Bk*+x8_!84rNsxSK$eAqp(G-Z0420X)AiFKu9fHK zUE?{Q-0Ct~P*7knTMhLOFI^U&m36le3I?J2RkXO8wR7h_@Q1@%na``fvUBIo@KoVn zf4yKDVW!Jm=dvCSW^dj4TK$v-0HCO-C{bX%>TjNSGCn1Fw$R~eht+%=C}lW*zS4j7 z+SN~!5@&yJ_3G8yWZ~a<;|)2i`7hX)KKw1S(JZ`s_Sc?JK+oR3eS5>rUN^2aux;D6 z7ClU|-~G+6+nQUPdR+80bab|%x6gy6_T`2-bLQON)ZDOdvhV;%Nb{e*_ujc#Gi@_z zbaXUX@yN0K0^A{I58@qWGXFKHNTi z1~D3qQG{se;}&27eR$EL#qpG(YBY(wyw@DTV32TdNTLi%DOA6LS&2zh5Xj>br7tSl zyU=7b>IQ=W{r$b@?)IRkrwgsEF8t=V@2aX_*|cq2{s-e02nGYmci(l7ux9PrQ6&lr z3JgRbL2y74B~XDvAOZ*xpzHdyiNF^Z@4cUj^z3&Y{H~>|s{^gAF0{3|;c~h7e}44Q zfErL&yporH@@6@15Fi2xXj&My88d~a)~%1H3{{bZR8UZ0utpgx9$CIZB7{KKbx_Jc zDFwI=&Iz3uymWzW*3%m3=G%)8lu&kM>Z7-gW8AruaSiL`D!@cUlh z_kh6IAC@dyVwGiyIGqiszH(LR>-V1N_YUNQgSaa%FaPuLxTG@vm{y{Ms&5$J- zgb--DhEOmFA`l3KfDi$M5D;N-0r&KF!DKRF_3AaFN)#3Sbl%9BzjF1O5(@JplZ78u z006vw>MM>$$7hw56{;i|U^ZJJ%Q7TUf-D(ekc|)}ap>wZCa#bA)&bT;7X zwJ#K%qdf1W?JrCP{-y$v*t2I(swj~k5Gp*bh4pCwr=lpS8 zX063yvEa{_FN1R(gZVfBg3QcJ2p~B1&M8D$P3Y+KC`Oav@V2}kzxY+aPe}j(cI@7f zZ4kxEwQJT{1jtJk$GlHWG^Q*73`$T@tX%)pdW$GY zsI6-N=Nh;^)C~gMnQp1cDzj#A|&{rlgX@Qg#`yZ4g_8K_u1c+l^VxJ=|PJ3p< zhQBA0WI&z60bSPy_elC+Co-l!+Bv^S6$=?HF%Nr~~Pbm+YGvAM?VNO-oG;?Fw2v3W6aG z)BRVFHeG<(B%`LTkvF&Xe`b|KX&X2GgVk)l4UNri=tGxSuIu37NJ~yaFzCmTqvhV9 z-)}E2E}c{-#@&ImRmJ-g<6~d^r%ex=T6+Q*@P`o$X$S;0I77*(aSh;ly%VaGNZzlB zVoD%jKYH}2*XQj+N>U;il|UH-Wg;kLa5TFh$woZAag&vbV#SuNTPD?saS3FsDk(~` z#pV8Jlik?b7eK!nLPw8+&Q>?Vst>*p#s4T-_&k&i^Nf4IH9fhsxU|ZzDE1>qk9Y_A zdyqWK21+F`CW0~%0%K@$wZULA;~zIYV-*Ejv1Lo{ge7h%kiM#@WR@-Vhq+JNP3?U_ zc$FZU+Xvuos)xlWLlBJ!1~mji8m3ug)H=HXBDevdwDdrg&+oS%EK#-%*=MAFI(*{xVAwI@{PcL6pxQ<2Si=P+zcDu``{DB(=+hC1C3 zTf7b5f9`oJlZ4;r$B@cToEj*bRI3p{=PL3n}V?#y(2SXJ%o=<4}h<9z>9 z4-=q51!qqFZ1=dv=NvmlflcW~ zTgJa9CC-e^NJ}Qxs0N;q&A2~aJ@txTi z9bm)i7>8<@hH^w|42;p;(3fwaVMT_wRv309xN*VrA%Wo|9BD1ah@l#6ME=MazIGyy zHzK)5@9b!`hGr`QJ^UCcFfzG*Gw3lyM#~?WB#sd^hC}~Fo^SqJOLw{{R30 literal 0 HcmV?d00001 diff --git a/src/www/static/app.css b/src/www/static/app.css new file mode 100644 index 0000000..5b28193 --- /dev/null +++ b/src/www/static/app.css @@ -0,0 +1,82 @@ +:root { font: 14px/1.45 "Lucida Grande", Verdana, "Bitstream Vera Sans", sans-serif; color: #111; background: #e4eaf4; } +* { box-sizing: border-box; } +html, body { margin: 0; min-height: 100%; } +body { background: #e7e4ef url('/assets/bg-page.png') repeat-x 0 0; } +button, input, select { font: inherit; } +button { cursor: pointer; } +[hidden] { display: none !important; } +.muted { color: #59677a; } +.error { color: #b00000; font-weight: bold; } + +/* Dragonfly login, retaining the original Bongo color and spacing model. */ +.login { position: absolute; top: 25%; left: 0; right: 0; min-height: 190px; padding: 2.8rem 1rem 1.2rem; text-align: center; color: #333; border-top: 2px solid #d4dceb; border-bottom: 2px solid #e4eaf4; background: #b4c5e1 url('/assets/login-bg-middle.png') repeat-x 0 0; } +.login-bg-top, .login-bg-bottom { position: absolute; left: 0; right: 0; width: 100%; pointer-events: none; } +.login-bg-top { top: -188px; height: 185px; border-bottom: 1px solid #385798; background: url('/assets/login-bg-top.png') repeat-x 0 100%; } +.login-bg-bottom { bottom: -296px; height: 293px; border-top: 1px solid #1d3980; background: url('/assets/login-bg-bottom.png') repeat-x 0 0; } +.brand { position: absolute; top: -76px; left: 50%; width: 339px; max-width: calc(100% - 2rem); height: auto; transform: translateX(-50%); } +.login-message { margin: 0 0 .8rem; font-size: 87%; } +#login-form { display: grid; grid-template-columns: max-content 15rem; gap: .45rem .75rem; justify-content: center; align-items: center; text-align: right; } +#login-form label { font-weight: normal; } +#login-form input { width: 15rem; min-height: 1.8rem; padding: .2rem .35rem; color: #000; background: #fff; border: 1px solid #7f8da5; } +#login-form .error, #login-form button { grid-column: 2; } +#login-form button { justify-self: end; padding: .25rem 1rem; color: #111; border: 1px solid #687a96; border-radius: 2px; background: linear-gradient(#fff, #c5d2e5); } + +/* Original Dragonfly page proportions, implemented with modern grid. */ +main { min-height: 100vh; } +.shell { min-height: 100vh; display: grid; grid-template-columns: 14.5rem minmax(0, 1fr); grid-template-rows: auto 1fr; } +header { grid-column: 1 / -1; display: flex; align-items: center; min-height: 68px; padding: 10px 15px; color: #fff; border-top: 4px solid #ffac35; background: #1d3980 url('/assets/top-gradient.png') repeat-y center; } +.wordmark { display: block; margin-right: auto; } +.wordmark img { display: block; width: 240px; max-width: 38vw; height: auto; } +#current-user { font-size: 1.15rem; font-weight: bold; } +.task-button { margin-left: .8rem; border: 1px solid #8b6f00; border-radius: 1rem; background: #eee; color: #333; font-weight: bold; } +.task-button.has-tasks { background: #ffd75a; color: #5c3900; } +.task-panel { margin: .5rem 0 1rem; padding: .8rem; border: 1px solid #d5b64c; background: #fffbea; } +.task-panel h2 { margin-top: 0; } +.task-panel ol { margin: 0; padding-left: 1.5rem; } +.task { margin: .4rem 0; } +.task > span { margin: 0 .8rem; color: #555; } +.task.critical { border-left: 4px solid #b00020; padding-left: .5rem; } +.task button { margin-left: .35rem; } +.settings-button { display: inline-block; margin-left: .5rem; padding: .1rem .45rem; border: 1px solid #7185a0; border-radius: 50%; background: #f4f6f9; color: #324d70; font-size: 1.2rem; line-height: 1.4; text-decoration: none; } +.settings-button:hover { background: #fff; color: #f57900; } +.settings-view { display: grid; grid-template-columns: minmax(180px, 240px) 1fr; gap: 1rem; } +.settings-view > h2 { grid-column: 1 / -1; } +.settings-nav { display: flex; flex-direction: column; align-items: stretch; } +.settings-nav button { padding: .6rem; border: 0; border-bottom: 1px solid #ccd5e0; background: #eef3f8; text-align: left; } +.settings-nav button.active { background: #bbceea; color: #1d3980; font-weight: bold; } +#settings-content { min-height: 14rem; padding: .8rem; border: 1px solid #ccd5e0; background: #fff; } +.settings-task-list { margin-top: 0; } +.quiet { margin-left: 1rem; padding: .25rem .6rem; color: #e4eaf4; border: 1px solid #7a9ac9; background: transparent; } +#section-sidebar { align-self: start; } +#sidebar-nav { min-width: 120px; margin: 12px 0 0 12px; padding: .4rem 0 1rem; background: #fff; border: 1px solid #5f85bf; border-width: 1px 0 1px 1px; } +#sidebar-nav a { display: flex; align-items: center; gap: .6rem; min-height: 30px; margin: .35rem 0; padding: 6px 12px 6px 18px; color: #517faf; font-weight: bold; text-decoration: underline; } +#sidebar-nav a:hover { color: #f57900; background: #f2f5fa; } +#sidebar-nav .tab:has(a.active) { color: #1d3980; border: 1px solid #95a9df; border-width: 1px 0; background: #bbceea url('/assets/bg-selected.png') repeat-x 50% 50%; } +#sidebar-nav a img { width: 22px; height: 22px; object-fit: contain; } +.workspace { min-width: 0; margin-top: 12px; padding: 1rem 1.25rem; background: #fff; border: 1px solid #95a9df; border-right: 0; } +.workspace > h1 { margin: 0 0 .25rem; color: #1d3980; font-size: 1.45rem; } +select { padding: .25rem; border: 1px solid #7f8da5; background: #fff; } +.small { margin: 0 0 0 .35rem; padding: .25rem .7rem; color: #111; border: 1px solid #687a96; background: linear-gradient(#fff, #c5d2e5); } +#conversation-list { margin: 1rem 0 0; padding: 0; list-style: none; border-top: 1px solid #afbdd2; } +#conversation-list li { padding: 0; border-bottom: 1px solid #ccd5e2; } +#conversation-list li.unread { font-weight: bold; background: #f4f7fb; } +#conversation-list button { display: grid; width: 100%; grid-template-columns: minmax(10rem, 1fr) 3fr auto; gap: 1rem; padding: .55rem; color: inherit; text-align: left; border: 0; background: transparent; } +.sender, .subject { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.message { margin-top: 1rem; padding-top: 1rem; border-top: 1px solid #afbdd2; } +.message-addresses { color: #59677a; overflow-wrap: anywhere; } +.message-text { white-space: pre-wrap; overflow-wrap: anywhere; } +.message-html { width: 100%; min-height: 20rem; border: 1px solid #afbdd2; background: #fff; } + +@media (max-width: 700px) { + .login { top: 20%; } + #login-form { grid-template-columns: 1fr; text-align: left; } + #login-form input { width: 100%; } + #login-form .error, #login-form button { grid-column: 1; } + .shell { grid-template-columns: 1fr; grid-template-rows: auto auto 1fr; } + header { grid-column: 1; } + #sidebar-nav { display: flex; margin: 0; overflow-x: auto; border-width: 0 0 1px; } + #sidebar-nav .tab { flex: 0 0 auto; } + #sidebar-nav a { margin: 0; } + .workspace { margin: 0; border-width: 0; } + #conversation-list button { grid-template-columns: 1fr; gap: .15rem; } +} diff --git a/src/www/static/app.js b/src/www/static/app.js new file mode 100644 index 0000000..3063a1e --- /dev/null +++ b/src/www/static/app.js @@ -0,0 +1,310 @@ +"use strict"; + +const loginView = document.querySelector("#login-pane"); +const appView = document.querySelector("#app-view"); +const loginForm = document.querySelector("#login-form"); +const loginError = document.querySelector("#login-error"); +let session = null; +let messages = {}; +let openConversation = null; +let composing = false; +let tasks = []; +const tr = text => messages[text] || text; + +function section() { + if (location.pathname === "/admin") return "admin"; + if (location.pathname === "/settings") return "settings"; + return location.hash === "#calendar" ? "calendar" : + location.hash === "#contacts" ? "contacts" : "mail"; +} + +function render() { + document.body.classList.toggle("login", !session); + loginView.hidden = Boolean(session); + appView.hidden = !session; + if (!session) return; + document.querySelector("#current-user").textContent = session.user; + document.querySelector("#compose-from").value = session.user; + const names = {mail: tr("Mail"), calendar: tr("Calendar"), contacts: tr("Contacts"), admin: tr("Administration"), settings: tr("Settings")}; + const messages = { + mail: tr("The secure webmail connection is being enabled."), + calendar: tr("Calendar access is available through CalDAV."), + contacts: tr("Contact access is available through CardDAV."), + admin: tr("Administrative functions require a server-side role check."), + settings: tr("Personal settings") + }; + const current = section(); + document.querySelector("#section-title").textContent = names[current]; + document.querySelector("#section-message").textContent = messages[current]; + document.querySelector("#mail-browser").hidden = current !== "mail" || Boolean(openConversation) || composing; + document.querySelector("#message-view").hidden = current !== "mail" || !openConversation || composing; + document.querySelector("#message-compose").hidden = current !== "mail" || !composing; + document.querySelector("#settings-view").hidden = current !== "settings"; + document.querySelectorAll("nav a").forEach(link => link.classList.toggle("active", link.dataset.section === current)); + if (current === "mail") loadFolders(); + if (current === "settings") renderSettings("tasks"); +} + +async function renderSettings(name) { + const content = document.querySelector("#settings-content"); + document.querySelectorAll(".settings-nav button").forEach(button => + button.classList.toggle("active", button.dataset.settings === name)); + if (name === "tasks") { + const response = await fetch("/api/tasks?include_closed=1", {credentials: "same-origin"}); + if (!response.ok) return; + const allTasks = (await response.json()).tasks; + const list = document.createElement("ol"); + list.className = "settings-task-list"; + list.replaceChildren(...allTasks.map(task => { + const item = document.createElement("li"); + item.textContent = `${task.title}: ${task.details.list_id || task.key} (${task.state})`; + return item; + })); + content.replaceChildren(list); + return; + } + const heading = document.createElement("h3"); + const button = document.querySelector(`.settings-nav button[data-settings="${name}"]`); + heading.textContent = button ? button.textContent : tr("Settings"); + content.replaceChildren(heading); +} + +async function loadFolders() { + const select = document.querySelector("#mail-folder"); + if (select.dataset.loaded) return loadConversations(); + const response = await fetch("/api/mail/folders", {credentials: "same-origin"}); + if (!response.ok) return; + const result = await response.json(); + select.replaceChildren(...result.folders.map(folder => { + const option = document.createElement("option"); + option.value = folder.path; + option.textContent = folder.name; + return option; + })); + select.dataset.loaded = "true"; + loadConversations(); +} + +async function loadConversations() { + const folder = document.querySelector("#mail-folder").value; + if (!folder) return; + const response = await fetch("/api/mail/conversations?folder=" + encodeURIComponent(folder), {credentials: "same-origin"}); + if (!response.ok) return; + const result = await response.json(); + const rows = result.conversations.map(message => { + const row = document.createElement("li"); + row.classList.toggle("unread", message.unread > 0); + const sender = document.createElement("span"); + sender.className = "sender"; + sender.textContent = message.from.map(item => item.name || item.address).join(", "); + const subject = document.createElement("span"); + subject.className = "subject"; + subject.textContent = message.subject || tr("(no subject)"); + const date = document.createElement("time"); + date.textContent = message.date; + const button = document.createElement("button"); + button.type = "button"; + button.addEventListener("click", () => loadConversation(message.id)); + button.append(sender, subject, date); + row.append(button); + return row; + }); + document.querySelector("#conversation-list").replaceChildren(...rows); +} + +function mailDocument(message, allowRemote) { + const parser = new DOMParser(); + const document = parser.parseFromString(message.html || "", "text/html"); + document.querySelectorAll("img[src]").forEach(image => { + const source = image.getAttribute("src"); + if (source.startsWith("cid:")) { + const data = message.inline_images[source.slice(4)]; + if (data) image.setAttribute("src", data); else image.removeAttribute("src"); + } else if (source.startsWith("bongo-remote:")) { + const index = Number(source.slice(13)); + if (allowRemote && message.remote_images[index]) image.setAttribute("src", message.remote_images[index]); + else { + image.removeAttribute("src"); + image.setAttribute("alt", image.getAttribute("alt") || tr("[external image blocked]")); + } + } + }); + return "" + document.body.innerHTML; +} + +function renderConversation(allowRemote = false) { + const container = document.querySelector("#message-items"); + let hasRemote = false; + const items = openConversation.messages.map(message => { + const article = document.createElement("article"); + article.className = "message"; + const title = document.createElement("h2"); + title.textContent = message.subject || tr("(no subject)"); + const addresses = document.createElement("p"); + addresses.className = "message-addresses"; + addresses.textContent = message.from.map(item => item.name ? `${item.name} <${item.address}>` : item.address).join(", "); + article.append(title, addresses); + if (message.html) { + const frame = document.createElement("iframe"); + frame.className = "message-html"; + frame.setAttribute("sandbox", ""); + frame.setAttribute("referrerpolicy", "no-referrer"); + frame.srcdoc = mailDocument(message, allowRemote); + article.append(frame); + } else { + const text = document.createElement("div"); + text.className = "message-text"; + text.textContent = message.text || ""; + article.append(text); + } + hasRemote ||= message.remote_images.length > 0; + return article; + }); + container.replaceChildren(...items); + document.querySelector("#load-remote-images").hidden = !hasRemote || allowRemote; +} + +async function loadConversation(id) { + const response = await fetch("/api/mail/conversation?id=" + encodeURIComponent(id), {credentials: "same-origin"}); + if (!response.ok) return; + openConversation = await response.json(); + render(); + renderConversation(false); +} + +async function loadSession() { + const response = await fetch("/api/session", {credentials: "same-origin"}); + session = response.ok ? await response.json() : null; + if (session) await loadTasks(); + render(); +} + +async function loadTasks() { + const response = await fetch("/api/tasks", {credentials: "same-origin"}); + if (!response.ok) return; + tasks = (await response.json()).tasks; + document.querySelector("#task-count").textContent = String(tasks.length); + document.querySelector("#task-button").classList.toggle("has-tasks", tasks.length > 0); + document.querySelector("#task-empty").hidden = tasks.length > 0; + document.querySelector("#task-list").replaceChildren(...tasks.map(task => { + const item = document.createElement("li"); + item.className = "task " + task.severity; + const title = document.createElement("strong"); + title.textContent = task.title; + const details = document.createElement("span"); + details.textContent = task.details.list_id || task.key; + item.append(title, details); + if (task.dismissible && task.severity !== "critical") { + for (const [state, label] of [["ignored", "Ignore"], ["disabled", "Disable"]]) { + const button = document.createElement("button"); + button.type = "button"; + button.textContent = tr(label); + button.addEventListener("click", () => setTaskState(task.id, state)); + item.append(button); + } + } + const done = document.createElement("button"); + done.type = "button"; + done.textContent = tr("Done"); + done.addEventListener("click", () => setTaskState(task.id, "completed")); + item.append(done); + return item; + })); +} + +async function setTaskState(id, state) { + const response = await fetch("/api/tasks/state", { + method: "POST", credentials: "same-origin", + headers: {"Content-Type": "application/json", "X-Bongo-CSRF": session.csrf_token}, + body: JSON.stringify({id, state}) + }); + if (response.ok) await loadTasks(); +} + +loginForm.addEventListener("submit", async event => { + event.preventDefault(); + loginError.hidden = true; + const button = loginForm.querySelector("button"); + button.disabled = true; + try { + const response = await fetch("/api/session", { + method: "POST", credentials: "same-origin", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify({user: loginForm.user.value, password: loginForm.password.value}) + }); + loginForm.password.value = ""; + if (!response.ok) throw new Error(response.status === 429 ? tr("Too many login attempts. Please try again later.") : tr("Login failed.")); + session = await response.json(); + render(); + } catch (error) { + loginError.textContent = error.message; + loginError.hidden = false; + } finally { button.disabled = false; } +}); + +document.querySelector("#logout").addEventListener("click", async () => { + if (!session) return; + await fetch("/api/session", {method: "DELETE", credentials: "same-origin", headers: {"X-Bongo-CSRF": session.csrf_token}}); + session = null; + render(); +}); +document.querySelector("#task-button").addEventListener("click", event => { + const panel = document.querySelector("#task-panel"); + panel.hidden = !panel.hidden; + event.currentTarget.setAttribute("aria-expanded", String(!panel.hidden)); + if (!panel.hidden) loadTasks(); +}); +document.querySelectorAll(".settings-nav button").forEach(button => + button.addEventListener("click", () => renderSettings(button.dataset.settings))); +document.querySelector("#mail-folder").addEventListener("change", loadConversations); +document.querySelector("#mail-refresh").addEventListener("click", loadConversations); +document.querySelector("#message-back").addEventListener("click", () => { openConversation = null; render(); }); +document.querySelector("#load-remote-images").addEventListener("click", () => renderConversation(true)); +document.querySelector("#compose-new").addEventListener("click", () => { + openConversation = null; + composing = true; + document.querySelector("#compose-status").textContent = ""; + render(); + document.querySelector("#compose-to").focus(); +}); +document.querySelector("#compose-cancel").addEventListener("click", () => { + composing = false; + document.querySelector("#message-compose").reset(); + render(); +}); +document.querySelector("#message-compose").addEventListener("submit", async event => { + event.preventDefault(); + const form = event.currentTarget; + const status = document.querySelector("#compose-status"); + const submit = form.querySelector("button[type=submit]"); + submit.disabled = true; + status.textContent = ""; + try { + const response = await fetch("/api/mail/send", { + method: "POST", credentials: "same-origin", + headers: {"Content-Type": "application/json", "X-Bongo-CSRF": session.csrf_token}, + body: JSON.stringify({to: document.querySelector("#compose-to").value, + cc: document.querySelector("#compose-cc").value, + bcc: document.querySelector("#compose-bcc").value, + subject: document.querySelector("#compose-subject").value, + text: document.querySelector("#compose-body").value}) + }); + if (!response.ok) throw new Error(response.status === 400 ? tr("Invalid message fields.") : tr("The message could not be sent.")); + form.reset(); + composing = false; + render(); + document.querySelector("#section-message").textContent = tr("Message sent."); + } catch (error) { + status.textContent = error.message; + } finally { + submit.disabled = false; + } +}); +window.addEventListener("hashchange", render); +fetch("/api/i18n").then(response => response.json()).then(catalog => { + messages = catalog.messages; + document.documentElement.lang = catalog.language.replace("_", "-"); + document.querySelectorAll("[data-i18n]").forEach(element => { + element.textContent = tr(element.dataset.i18n); + }); +}).finally(() => loadSession().catch(() => render())); diff --git a/src/www/static/bg-page.png b/src/www/static/bg-page.png new file mode 100644 index 0000000000000000000000000000000000000000..5f81a281cb6a037b4a90b453191c49e4f19f2c52 GIT binary patch literal 9911 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 literal 0 HcmV?d00001 diff --git a/src/www/static/bg-selected.png b/src/www/static/bg-selected.png new file mode 100644 index 0000000000000000000000000000000000000000..cbba7d5d9a015d3f3874b417b281d8b006dbb61b GIT binary patch literal 510 zcmeAS@N?(olHy`uVBq!ia0vp^GC-Wd!3HF!)&y7pDYhhUcNd2LAh=-f^2tCE&H|6f zVg?3&D8_`iF1B#Zfaf$gL6@8Vo7R> zLV0FMhC)b2s)DMBZ^Ipn4lD4)-2C(3`mgi+OY$W`TWn2K zdXi&aB-$z_sR^^|-cwksvZ4RHzLSMmhr0k{^#w081g&xw??^el z(r^o3`c|3i?cV3R-8_yLo;;%En>P9R!aGYpPdD3{A#|kYbN90ri9=gkUTcZS}IjvS?dXXB#z3E%y%5&ZyU;5rV?0A6AtT$JLCVe_G z{e6H>#o1ezzOlLY_!PAljgre35X`or(tHSaUem{&<3Ru$TA$JS6+{q5M( z)2rQ{_rQxwhpC~pa=zr>~V8&aJ3>Kz9CZr@x@$+9 z6WS?lf9$!v6+;=Hwl;HX=XsvgtJ}e9ukR*BBLKg9f8?S4nDU$5)zXIa&-1jP=Tz{6 z>h;}Za^q0|K!6|Ns~CX*m*(eLy7fd5IK6P@EoU|b0Ij8ZP+C`_5kd3p;3oi!i*?M9 z$uGy@>B;Gow{`M;5cTdA1PJ)^9337LTZmErTqm{=DR zx7(%BXiRyNQj#L>=C(Kj(k2(B9S1SPE+371t1cRfEHf_nQkr= z3Yexzr_&*m$&9vJ_wHjoun@WEBk(C2jRH`qRAy4WeYZ~}5+0A>0et=TWoDD7n`<`! zc=7xtA`w4<9v%F@hOeLnjYf7dmjM~whadwqkzw&Z?a$CpCzxNAs bLG%4z-ZZJyf=;2700000NkvXXu0mjfO>XpU literal 0 HcmV?d00001 diff --git a/src/www/static/contact-new.png b/src/www/static/contact-new.png new file mode 100644 index 0000000000000000000000000000000000000000..2640eaf58f4c12cdd788927c9ae79b5ae1d27730 GIT binary patch literal 2457 zcmV;K31;?*P)D;LA1IC1X_PaH=otCJNma+G;yCMS23-^x zrUJw6z_b>j?PkQRM~n)T@&Ov9Qz$DAg&}M+BAKjD1?hupfPlP*4`nX`e+O8LF|0O} z)rgqYDEy!_xJl44Fsm^0esTdMnN6z*v8zsiMjA2g7BCl~><+~0K-sOR1qK8Sa0E5B z0G+7AAczJGokQD6kwW~c5|G!hFzh-o7oe;r#A?F`wZdW>qsfA>0te)ri_uqw9=riX z2HX*FV+>Jsbu+MT4a8P#Cag4qBq7Dl^`x!8gl6S9JwbZ`M9S%Ds|N6Z?;s>IG! zPa@FW-F=^J+YjgS`6dy$;wRK|zZ;{z_7rv1L)6!urM6}q(~4qPjhHM#nTtS@xCKEw zBe;dL7&LtSCxf^4o(LRoZ~oU2qv*@$^Zjzeh7F%Oco9Ks zjn*31b*ELwaVQiDI1V_D!f_2;*Fbb0krW~c&}Tru3%U=HVMN9e$pSu_8gH=AoL%yD z3?o&_fa5qVfk5C>1zxn?_DohpKmsTo#2^Gj5Gqg-2Ym_w&_kTx#4*q@MB->!gk@Ke z%A~6xH|Z3bwdOxM`Z{m?^##U9W3(>4g*!HGCQ?;Db8zz^KnSQrOBDt+h#CVufHL|( zCxNmF0%M4rM(bKMHJGY~OlA}!jLhp4`u)>CeMNs(daCgdJN)d5+GR=g3{+ud<4n{G@?y}S~LrAbpy5$Wn?TVO5x5);GKW| ziQ|V}<+ha_1Z@y;$>b{;iVgGp&+p;EUmwNiFDnIf@ZdrE`}ksJr_$bS6Y-RXdg7R`d0e>Ax0Z@np5h`(Y zEn1qvSVlT~MqnPF68oO}2@4ymDYG@zm0=WcE0$ZuK`%Ss)}JTuuEmiyyr=Q384wX76}60yrwAA}XX9}cit$tm z#})D#D(hP()pSo!4mfHi@c{4SX> z1I3_j`s#P+JuyToXAp}gICFNGM8+kVaTp#+@`W#Nnv{Xdqb6E{XbGbwh=@7q`K$!I zz<1s~z;FJ%n~_4EqsKqt^hlO?N;n-)alE&mPaMrluk5@61ZG#STu?$qz{qHfhqry3 z6`yOtva-ZJ93(lI!Oc0iev4G4j}^FoXn{nKn8QA;cHtOoiFw89R(LU^^0{ITjSc^t4)P(9&j3!RdzPN#Be)S_9 zr%)QJl)50usVg{jHK$wQxmN#z40u6@-~0$%?KGZOHE#>dAns3x6u2n4{;f>K2cy!rMk zAQnEmhCsl_z{wBkXg`0_^!N9ZPN!!qq3MDO27@#;HC;Xe?Kj-S(9j8Lsv8+g^n*K@ zfao-q8ANG6$;1ftb!{`Id$z#6{nM4%B@?*sd)wIZ;N65HQJSNx5u8aUuz|wSih&bD z)YZ0e`bn1Pi%y7d))`_eD?;P`tKoWl8~pAe-K;cz)$x^)xZ`sR-> z2;x!*%oj5&0k4y#OK#?8KYjVKZo(^0V7{rmPT(>TnDIrnefxGqgeRYTlE)r<3=!e+ z#~&}*uC6Xbq-1yO*ijn$#1l`HY_Yv_=T3I++*$JJwYzrhD%q!=daC4S_wL>7-o3lj zzNidLWjtM0W;#K<&t}8U$L~xlKPv&P^^5^s%;a;G$6WaNNa>G!Pft%~^XAQgWHO1? zny#)cwrttL=;$b}>kEn{$SkXS56Lqh`%4GrY;dER>KEwt8zLLqLy{dNu< zI6!M_EAe=ov9U20ELgy*RjcNef%N_N-ycjQ5~WoXk<#h`IF5s9p8s>H?0(PSwJM<|eE=EvfmE zpHOeWMKZ;p%?XU*!Fvf5t@c=VLJL!ZcRjc{1G`+}4S1?=nWlYdo8Y=!m+Nv}t||E+ Xn~4)K36gfg00000NkvXXu0mjfXN;hn literal 0 HcmV?d00001 diff --git a/src/www/static/css/admin.css b/src/www/static/css/admin.css new file mode 100644 index 0000000..c9ef0aa --- /dev/null +++ b/src/www/static/css/admin.css @@ -0,0 +1,582 @@ +/* + Bongo web administration interface (aka Hawkeye) stylesheet + (c) 2007 Alexander Hixon - see COPYING for details + Last modified: Monday Sep 3, 2007 + + =======[ Global Styles ]======= +*/ + +body { + background-color: #eeeeec; + font-size: 16px; + font-family: luxi sans, trebuchet ms, bitstream vera sans, sans-serif; + color: #eeeeec; + padding: 0px; + margin: 0px; +} + +#body-override { + color: #2e3436; + background: url('/img/bg.png'); + background-repeat: repeat-x; + background-color: #fff; + margin-top: 25px; + margin-left: 60px; + margin-right: 60px; +} + +/* + =======[ Login Styles ]======= +*/ + +#login { + font-size: 110%; + position:absolute; + height:300px; + width:400px; + margin:-150px 0px 0px -200px; + top: 50%; + left: 50%; + + text-align: center; + padding: 0px; + background-image: url('/img/login-pane.png'); + /*background-repeat: no-repeat; /* Make sure this doesn't actually happen. */ + overflow: none; +} + +#login form { + padding: 16px; + padding-top: 6.5em; +} + +.inputbox { + width: 178px; /* 200px - 22px */ + margin-bottom: 2px; +} + +#login select { + width: 202px; +} + +.submit { + padding-top: 1em; + padding-bottom: 0px; + margin-bottom: 0px; +} + +input, select { + color: #2e3436; + background-color: #fff; + border: 1px solid #888a85; +} + +.lang { + background-image: url('/img/locale.png'); +} + +.username { + background-image: url('/img/user.png'); +} + +.password { + background-image: url('/img/locked.png'); +} + +.password, .username, .lang { + padding-left: 22px; + min-height: 18px; + background-repeat: no-repeat; +} + +/* + =======[ Main Styles ]======= +*/ + +#userinfo { + padding-top: 20px; + font-size: 80%; + float: right; +} + +#logo { + margin-bottom: 60px; +} + +p { + font-size: 90%; + color: #2e3436; /* Tango DARK-GREY */ +} + +a { + text-decoration:none; + color: #2e3436; +} + +#page { + background: url('/img/cbg.png'); + background-repeat: repeat-x; + background-color: #eeeeec; /* Tango GREY */ + /*margin-left: 155px; /* Push across width of sidebar, minus 10px, for bg behind round corner */ + /*padding-left: 20px; /* Prints BG for 10px, now flush with sidebar */ + margin: 0px; + padding: 0px; + margin-left: 175px; + /*float: right;*/ + padding-top: 6px; + min-height: 390px; + + -moz-border-radius-bottomright: 20px; + -khtml-border-radius-bottomright: 20px; + border-radius-bottomright: 20px; +} + +#content { + padding-left: 12px; + padding-right: 12px; +} + +.breadcrumb { + font-size: 75%; +} + +.floaty { + float: right; + padding-left: 16px; + padding-right: 16px; + display: block; +} + +#wrap { + background: url('/img/cbg.png'); + background-repeat: repeat-x; + background-color: #eeeeec; /* Tango GREY */ + /*margin-left: 155px; /* Push across width of sidebar, minus 10px, for bg behind round corner */ + /*padding-left: 20px; /* Prints BG for 10px, now flush with sidebar */ + margin: 0px; + padding: 0px; + /*padding-left: 175px;*/ + /*padding-top: 6px;*/ + + -moz-border-radius-bottomleft: 20px; + -khtml-border-radius-bottomleft: 20px; + border-radius-bottomleft: 20px; + + -moz-border-radius-bottomright: 20px; + -khtml-border-radius-bottomright: 20px; + border-radius-bottomright: 20px; + + padding-bottom: 6px; + margin-bottom: 1em; +} + +#footer { + margin-bottom: 1em; + text-align: right; + padding-right: 2em; + font-size: 85%; + display: block; +} + +/* + =======[ Sidebar Styles ]======= +*/ + +#sidebar-wrap { + float: left; + width: 175px; + + /*height: 500px;*/ + + margin: 0px; + padding: 0px; + + background: url('/img/sbg.png'); + background-repeat: no-repeat; + + color: #eeeeec; /* Tango WHITE */ + + text-align:center; +} + +nlink a, a.nlink { + color: #eeeeec; + border-bottom: 0px none; +} + +#sidebartoggle { + display: inline; + position:relative; + top: -50px; +} + +#sidebar { + height: 400px; + /*height: 100%; /* Always make sidebar as big as content. */ + + /*background-color: #2e3436; /* Tango DARK-GREY */ + + text-align: right; + margin: 0px; + padding: 0px; + /*margin-right: 0px; + padding-right: 0px; + padding-bottom: 6px;*/ +} + +.helpitem { + /* FIXME! */ +} + +.selecteditem { + background-image: url('/img/sel.png'); + width: 175px; + background-repeat: no-repeat; + color: #2e3436; +} + +.disabled { + color:#aaa !important; +} + +ul.nav, +.nav ul{ + margin-right: 0px; + padding-left: 4px; + margin-left: 6px; + padding-right: 0px; + list-style-type: none; +} + +a.nav, +.nav a{ + color: #eeeeec; + text-decoration:none; +} + +a.selecteditem, +.selecteditem a:link, +.selecteditem a:visited { + color: #2e3436; + text-decoration:none; +} + +li.nav, +.nav li{ + display: block; + font-size: 115%; /* Bold maybe? */ + min-height: 35px; + text-align: left; + + padding-left: 15%; + padding-right: 15px; + margin-bottom: 15px; + text-decoration:none; + margin-top: 5px; + width: 95%; +} + +img.icon { + margin-right: 6px; + vertical-align:middle; +} + +ul.nav{ + width: 100%; +} + +/* + =======[ Table Styles ]======= +*/ + +.htable { + border: 3px solid #729fcf; + -moz-border-radius: 10px; +} + +.hrow { + color: #d3d7cf; + font-weight: bold; + padding-left: 4px; +} + +.highlight, .highlight0 { + color: #d3d7cf; + background-color: #204a87; +} + +.highlight1 { + color: #d3d7cf; + background-color: #3465a4; +} + +.drow { + color: #d3d7cf; +} + +/* + =======[ Message Notification Styles ]======= +*/ + +.error, .info { + -moz-border-radius: 5px; + + margin-right: 12px; + min-height:18px; /* Icon size - total padding (top & bottom) */ + padding-top: 8px; + padding-bottom: 8px; + + padding-left: 36px; + background-repeat: no-repeat; +} + +.error { + background-image: url('/img/dialog-error-32.png'); + background-color: #a40000; + color: #eeeeec; + border: 3px solid #cc0000; +} + +.info { + background-image: url('/img/dialog-information-32.png'); + background-color: #3465a4; + color: #eeeeec; + border: 3px solid #204a85; +} + +/* + ========[ Icon View ]======= +*/ + +#iconview { + text-align:center; + vertical-align:middle; +} + +#iconview div.icon { + margin:3px; +} + +#iconview div.icon a { + border:1px solid #d3d7cf; + display:block; + float:left; + height:97px !important; + padding:2px 5px 1px; + text-decoration:none; + vertical-align:middle; + width:108px !important; +} + +#iconview div.icon a:link { + color:#888a85; +} + +#iconview div.icon a:hover { + background-color:#d3d7cf; + border:1px solid #2e3436; + color:#2e3436; + padding:3px 4px 0px 6px; +} + +#iconview div.icon a:active { + color:#888a85; +} + +#iconview div.icon a:visited { + color:#888a85; +} + +#iconview div.icon img { + margin-top:13px; +} + +#iconview div.icon span { + display:block; + padding-top:3px; +} + +/* + =======[ Button Styles ]======= + http://www.hedgerwow.com/360/dhtml/css-round-button/ +*/ + +a.button, span.button, del.button { + display:-moz-inline-box; + display:inline-block; + cursor:pointer; + border:none; + font-size:0; + line-height:0; + + /* + for Safari, read this first + http://creativebits.org/webdev/safari_background_repeat_bug_fix + */ + background-position:0 0; + background-repeat:no-repeat; + height:30px; + text-decoration:none; + color:#2e523b; + font-style:normal; + margin:0 6px 0px 0; + padding:0 10px 0 0; + vertical-align:middle; + + padding-top:-2px; + _position:relative; + _width:10px; + _overflow-y:hidden; +} + +a.button, span.button, del.button, +a.button span, span.button button, span.button input, del.button span { + background-image:url('/img/form_buttons.png'); + _background-image:url('/img/form_buttons.gif'); +} + + +a.button span, span.button button, span.button input, del.button span { + white-space:nowrap; + cursor:pointer; + color:#222; + display:-moz-inline-box; + display:inline-block; + line-height:1; + /*letter-spacing:0 !important; + font-family:"Arial" !important;*/ + font-size:12px !important; + font-style:normal; + background-color:transparent; + background-position:100% 0; + background-repeat:no-repeat; + height:30px; + padding:8px 20px 0 10px; + margin:0 -16px 0 10px; + border:none; + vertical-align:text-top; + zoom:1; + _position:relative; + _padding-left:0px; + _padding-right:12px; + _margin-right:-10px; + _display:block; + _top:0; + _right:-5px; +} + + +span.button button { + line-height:2.5; /*Opera need this*/ +} + +html.safari a.button span, +html.safari del.button span { + line-height:1.3; +} + +html.safari span.button button { + line-height:2.6; +} + +html.safari a.button:focus, +html.safari span.button button:focus { + outline:none; +} + +del.button { + /* cursor:not-allowed; */ + background-position:0 -120px; +} + +del.button span { + cursor:default; + color:#aaa !important; + background-position:100% -120px; +} + +span.button button, span.button input { + padding-top:0px; + line-height:2.5; /*Opera need this*/ +} + +/*Hover Style*/ +a.button:hover, +span.button:hover, +a.button:focus, + +a.dom-button-focus, +span.button-behavior-hover { + background-position:0 -60px; + color:#222; + text-decoration:none; +} + +a.button:hover span, +span.button:hover button, +a.button:focus span, + +span.button-behavior-hover button, +span.button-behavior-hover input { + background-position:100% -60px; +} + +a.button:active, a.button:focus span { + color:#444; +} + +del.button-behavior-hover, del.button:hover { + background-position:0 -180px; + /* cursor:not-allowed; */ +} + +del.button-behavior-hover span, del.button:hover span { + background-position:100% -180px; + /* cursor:not-allowed; */ + +} + +/*Optional hack for IE6 to simulate :hover selector*/ + +span.button button, del.button span, span.button input{ + + + + _behavior:expression( + (function(el){ + + if( typeof( behavior_onMouseEnter) == 'undefined'){ + + + behavior_onMouseEnter = function(el){ + + var dEl = this.parentNode; + var sClass = dEl.className ; + dEl.__defaultClassName = sClass ; + dEl.className = sClass + ' button-behavior-hover'; + this.setCapture(); + }; + + behavior_onMouseLeave = function(el) { + var dEl = this.parentNode; + dEl.className = dEl.__defaultClassName ; + dEl.__defaultClassName = undefined; + this.releaseCapture(); + }; + + + }; + + + el.runtimeStyle.behavior = 'none'; + el.onmouseenter = behavior_onMouseEnter; + el.onmouseleave = behavior_onMouseLeave; + + + })(this)); + + + + +} + diff --git a/src/www/static/css/calendar.css b/src/www/static/css/calendar.css new file mode 100644 index 0000000..fdfd157 --- /dev/null +++ b/src/www/static/css/calendar.css @@ -0,0 +1,1070 @@ +#calendar h1, #calendar h2 { text-align: center; } + #calendar h1 { padding: 0.5em; font-size: 16pt; } + #calendar h2 { padding: 7px; } + +/* Generic calendar table styles */ +.calendar { + width: 100%; + height: 100%; + position: relative; + z-index: 5; +} + + /* Modify columnview for the timestrip */ + .calendar.columnview.top, + .calendar.columnview.bottom { + /* + margin-left: 0; + */ + } + + /* Generic calendar rules for columnview and monthview */ + .calendar th, .calendar td { + vertical-align: top; + } + .calendar th { + font-size: 80%; + color: #555; + background-color: transparent; + border-left: 1px solid #eee; + border-bottom: 1px solid #ddd; + font-weight: normal; + } + + .calendar th.last { + border-right: 1px solid #eee; + } + + .calendar td { + border-left: 1px solid #ccc; + border-bottom: 3px double #ccc; + } + .calendar.monthview td { + /* Work around double-border on the left in monthview */ + border-right-width: 0; + border-left-width: 1px; + } + .calendar td.last { + border-right: 1px solid #ccc; + } + + .calendar.bottom td { + border-left: 1px solid #d5d5dd; + border-bottom: none; + } + .calendar.bottom td.last { + border-right: 1px solid #d5d5dd; + } + + .calendar .buble { + font-size: 86%; + list-style: none; + } + + /* Hide timezones and notes for items by default */ + .calendar .tz, .calendar .note { visibility: hidden; } + + /* Show notes on first event only */ + .columnview .timed-events .buble.start .note { visibility: visible; } + + /* Hide spacers */ + .calendar .spacer { visibility: hidden; } + + .calendar .date a, + .calendar th a { + color: inherit; + } + .calendar .date a:hover, + .calendar th a:hover { + text-decoration: none; + color: #000; + } + + .calendar .date a { padding: 2px 5px; } + + .calendar th a { + display: block; + padding: 3px 4px; + } + .calendar th a:hover { + background: transparent url(../img/cal-selected.png); + } + /* IE-only stipple */ + .eng-msie.engv-6 .calendar th a:hover { + background-image: none; + background-color: #eef; + } + +/*** Monthview-specific ***/ + +.monthview { + border-bottom: 1px solid #d5d5dd; + padding: 0 6px; +} + + .calendar td.offmonth { + background: #fcfcfc url(../img/cal-offmonth.png); + color: #bababa; + } + .calendar.cellview td.offmonth { background-image: none; } + + .calendar tr > td.today { + background-image: url(../img/cal-today.png); + color: #b99508; + } + /* IE6 stipple BG for today */ + .eng-msie.engv-6 .calendar td.today { + background-image: url(../img/cal-today-stipple.png); + color: #b99508; + } + .calendar td.offmonth.today { + color: #675922; + } + .monthview td { + height: 8em; + width: 14.28%; + padding: 0 0 1.5em; + border-bottom: 1px solid #d5d5dd; + color: #777; + } + .monthview tr.last td { border-bottom: none; } + + .monthview .date { } + html>body .monthview .date { text-align: right; font-size: 100%; } + + .monthview .timed-events .buble { + background-color: transparent; + width: auto !important; + height: auto !important; + padding: 0 0.1em 0 0.5em; + list-style: disc; + list-style-position: inside; + background-color: transparent; + } + + .monthview .untimed-events .buble { height: auto !important; } + + /* Hide non-starting day-spanning timed events */ + .monthview .timed-events .buble { display: none; } + .monthview .timed-events .buble.start { display: list-item; } + + /* Webkit browsers need the width to be 100% instead of auto */ + .eng-webkit .monthview .timed-events .buble { width: 100%; } + + /* "Simple" fix for IE7 */ + .eng-msie.engv-7 .monthview .untimed-events { + position: relative; + width: 100%; + } + + /* Fix for IE6 */ + /* _REAL_ FIX COMING SOON! (This is a quick placeholder) */ + .eng-msie.engv-6 .monthview .untimed-events { position: relative; } + + /* Turn off non-titles in month view */ + .monthview .item span { display: none; } + + .monthview .timed-events .item .time { + display: inline; + opacity: 0.6; + font-size: 90%; + } + /*.monthview .timed-events .item .time:before { content: " ("; }*/ + .monthview .timed-events .item .time:after { content: ": "; } + .eng-msie.engv-7 .monthview .timed-events .item .time, + .eng-msie.engv-6 .monthview .timed-events .item .time { margin-right: 1ex; /* IE-only */ } + + .monthview .untimed-events .item, + .monthview .untimed-events .spacer { + height: 1em; + line-height: 1; + overflow: hidden; + padding: 2px; + border: 1px solid red; + margin: 1px 0; + } + + .monthview .untimed-events .item strong { + /* Add space between the lines */ + line-height: 2em; + position: relative; + bottom: 0.5em; + padding-left: 0.25em; + } + + .month-header { + width: 100%; + } +.month-header-title { + width: 100%; +} + +/*** Summary-specific ***/ +.cellview { + border: 1px solid #ccc; + border-collapse: collapse; +} + +.cellview td { + width: 50% !important; +} + +.cellview .date { + text-align: center !important; + padding-top: 5px; + padding-bottom: 5px; + text-decoration: underline; +} + + +/*** Columnview-specific ***/ +.columnview { } + + .calendarwrapper .top { margin: 0; } + + .calendarwrapper .bottom { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + } + .columnview td.untimed { + background-color: #eee; + } + .columnview td.untimed .spacer, + .columnview td.untimed .buble { + position: relative; + display: block; + font-size: 0.9em; + height: 1.75em !important; + line-height: 1.7; + overflow: hidden; + } + + .columnview .timed-events { + /* Wrapper to have event children be absolutely position relative to column */ + position: relative; + /*height: 72em;*/ + } + + .columnview .timed-events .buble { + list-style: none; + position: absolute; + /* We don't want disappearing events... */ + min-height: 1.5em; + overflow: hidden; + height: 8.34% + } + + +/*** Both columnview & monthview ***/ +.calendar {} + + /* Make titles non-bold */ + .calendar .item strong { font-weight: normal; } + + +/*** Event colors ***/ +#calendar .buble { +} + + .calendar .buble { color: #fff; } + #calendar-list .cal-name { + padding-left: 4px; + /* + overflow: hidden; + white-space: nowrap; + */ + } + + /* Hack to fill the buble with the item (for IE6) */ + /* It's approximate, but works pretty ok... */ + .eng-msie.engv-6 #calendar .columnview .buble .item { width: 90%; height: 95%; } + +/**************/ +/*** Bubles ***/ +/**************/ + +/* transitory event styles */ +.buble.moving { + opacity: 0.5; + z-index: 10; + position: relative; +} + +.buble.deleting { + border: 2px solid red; +} + +.buble.saving { + border: 2px solid green; +} + + +.columview .timed-events .buble.moving { + position: absolute; +} + +/* All bubles, regardless of view */ +.monthview .untimed-events .spacer, +.monthview .untimed-events .buble .item, +.columnview td .buble .item { + overflow: hidden; + display: block; +} + +/* ...except don't display dragholders in month view */ +.monthview .untimed-events .dragholder {display: none; } + + /* columnview only */ + .columnview td .buble .item { + position: absolute; + bottom: 0; + right: 0; + left: 0; + top: 0; + } + + .columnview ul .buble .item { + padding: 3px; + } + + .columnview .timed-events .buble .item { + margin: 0 2px 0 2px; + + /* the top and bottom borders are handled by the selectors + * with .start and .end so that events spanning multiple days + * do not have borders on the portion that spans the day + * boundaries. + * + * The border + padding should equal 3px, so that the layout + * on the inside doesn't change just because you selected an + * event. + * + * Garrett, please check multi-day events if you change these + * to not be a border-left and border-right. + */ + border-left: 1px solid red; + border-right: 1px solid red; + padding-left: 2px; + padding-right: 2px; + } + + .columnview .timed-events .buble.popped-out .item { + border-left-width: 2px; + border-right-width: 2px; + padding-left: 1px; + padding-right: 1px; + } + + /* column view timed start */ + .columnview .timed-events .buble.start .item { + -moz-border-radius-topleft: 10px; + -moz-border-radius-topright: 10px; + border-top: 1px solid red; + padding-top: 2px; + margin-top: 1px; + } + .columnview .timed-events .buble.start > .item { + /* + background: url(../img/grad-cal-title.png) repeat-x 0 -480px; + */ + } + .columnview .timed-events .buble.start.popped-out .item { + border-top-width: 2px; + padding-top: 1px; + background-position: 0 -482px; + } + + /* column view timed end */ + .columnview .timed-events .buble.end .item { + -moz-border-radius-bottomleft: 10px; + -moz-border-radius-bottomright: 10px; + border-bottom: 1px solid red; + padding-bottom: 2px; + margin-bottom: 1px; + } + .columnview .timed-events .buble.end.popped-out .item { + border-bottom-width: 2px; + padding-bottom: 1px; + } + + /* Make the cursor a normal pointer */ + #calendar table ul .buble .item, + #calendar table ul .buble .item * { + cursor: default; + cursor: move; + cursor: grab; + cursor: -moz-grab; + } + + #calendar table ul .buble.moving .item, + calendar table ul .buble.moving .item * { + cursor: grabbing; + cursor: -moz-grabbing; + } + +/* Column view bubles' titles */ +.columnview td .buble .item .title { + font-family: luxi sans, myriad, bitstream vera sans, sans-serif; + display: block; + text-align: center; + margin: -0.2em; + margin-bottom: 0.25em; + /* + background: url(../img/stipple-1bit.png); + */ +} +.columnview .timed-events .buble .item .title { + padding: 0.4em 0.1em; +} + +.columnview td .buble .item > .title { +} +.columnview td .buble .item .time { + text-align: center; + display: block; + font-weight: normal; + letter-spacing: 1px; + font-size: 90%; + opacity: 0.75; + /* Let's hide time by default... */ + display: none; +} +.columnview td .buble .item > .inborder { + border: 2px solid #fff; + -moz-border-radius: 2px 2px 1px 2px; + border-radius: 2px 2px 1px 2px; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + opacity: 0.8; +} +.columnview td .buble .item .note { + display: block; + font-size: 90%; + opacity: 0.8; + padding: 0.5em 6pt; +} + +.columnview .buble.end .item .resize { + background: transparent url(../img/grippy-v.png) no-repeat bottom center; + height: 12px; + display: block; + position: absolute; + bottom: 0; + right: 0; + width: 100%; + margin: 0 -1px -1px; + opacity: 0.5; + cursor: resize !important; +} +.eng-msie.engv-6 .columnview .buble.end .item .resize { background-image: url(../img/grippy-v.gif); } +* +.columnview .buble.end.popped-out .item .resize { + opacity: 1; + margin: 0 -1px -3px; +} +.columnview .untimed-events .buble.end .item .resize { + background: transparent url(../img/grippy-h.png) no-repeat right; + width: 12px; + height: 100%; + margin: 0; +} +.columnview .untimed-events .buble.end .item .resize { background-image: url(../img/grippy-h.gif); } + +.columnview .untimed-events .buble.end.popped-out .item .resize { + margin: 0 -2px 0; +} + +.columnview td .buble .item > .resize { + /* Non-IE6 */ +} + +.columnview .item { + padding-top: 2px; + padding-bottom: 2px; + padding-left: 5px; +} + +.columnview .time { + display: block; + text-align: center; +} + +.calendar.view7days th, +.calendar.view7days td { + width: 14%; +} + +.calendar.view4days th, +.calendar.view4days td { + width: 25%; +} + +.calendar.view1days th, +.calendar.view1days td { + width: 100%; +} + +.calendarwrapper { position: relative; width: 100%; } + +.timedwrapper { position: relative; } + + +/*** Untimed continuation "rules" ***/ +.calendar .untimed-events .item { + background-repeat: no-repeat; + background-image: url(../img/continued-right.png); + background-position: right; + margin: 1px 0; + padding-top: 2px 3px; + border-top: 1px solid red; + border-bottom: 1px solid red; +} + .calendar .untimed-events .item strong { + background-repeat: no-repeat; + background-image: url(../img/continued-left.png); + background-position: left; + padding-left: 16px; + } + + .calendar .untimed-events .buble.popped-out .item { + border-top-width: 2px; + border-bottom-width: 2px; + padding-top: 1px; + padding-bottom: 1px; + } + + .calendar .untimed-events .buble.start .item { + -moz-border-radius-topleft: 9px; + -moz-border-radius-bottomleft: 9px; + border-left: 1px solid red; + padding-left: 2px; + margin-left: 1px; + } + .calendar .untimed-events .buble.start.popped-out .item { + border-left-width: 2px; + padding-left: 1px; + } + + .calendar .untimed-events .buble.end .item { + -moz-border-radius-topright: 9px; + -moz-border-radius-bottomright: 9px; + border-right: 1px solid red; + padding-right: 2px; + margin-right: 1px; + } + .calendar .untimed-events .buble.end.popped-out .item { + border-right-width: 2px; + padding-right: 1px; + } + + .calendar .untimed-events .buble.start .item strong, + .calendar .untimed-events .buble.end .item { + background-image: none; + padding-left: 2px; + } + + .calendar .untimed-events .buble.end .item strong { + } + .calendar .untimed-events .buble.start.end .item, + .calendar .untimed-events .buble.start.end .item strong { + background-image: none; + } + + /* Gray */ + #calendar-list .gray label { border-color: #aaa; } + #calendar .columnview .buble.gray .item, + .monthview .untimed-events .buble.gray .item { + background-color: #aaa; + border-color: #888; + } + #calendar-list .gray, + .monthview .timed-events .buble.gray { color: #888; } + #calendar-list .selected.gray, + .monthview .timed-events .buble.gray.popped-out { background-color: #f6f6f6; } + + /* Red */ + #calendar-list .red label { border-color: #e15e50; } + #calendar .columnview .buble.red .item, + .monthview .untimed-events .buble.red .item { + background-color: #e15e50; + border-color: #cc3525; + } + #calendar-list .red, + .monthview .timed-events .buble.red { color: #cc3525; } + #calendar-list .selected.red, + .monthview .timed-events .buble.red.popped-out { background-color: #fae4e1; } + + /* Orange */ + #calendar-list .orange label { border-color: #ee8d03; } + #calendar .columnview .buble.orange .item, + .monthview .untimed-events .buble.orange .item { + background-color: #ee8d03; + border-color: #ae6600; + } + #calendar-list .orange, + .monthview .timed-events .buble.orange { color: #e08400; } + #calendar-list .selected.orange, + .monthview .timed-events .buble.orange.popped-out { background-color: #fff1dd; } + + /* Yellow */ + #calendar-list .yellow label { border-color: #ebbd08; } + #calendar .columnview .buble.yellow .item, + .monthview .untimed-events .buble.yellow .item { + background-color: #ebbd08; + border-color: #c9a204; + } + #calendar-list .yellow, + .monthview .timed-events .buble.yellow { color: #eabb00; } + #calendar-list .selected.yellow, + .monthview .timed-events .buble.yellow.popped-out { background-color: #fff9e1; } + + /* Brown */ + #calendar-list .brown label { border-color: #d29940; } + #calendar .columnview .buble.brown .item, + .monthview .untimed-events .buble.brown .item { + background-color: #d29940; + border-color: #aa7d2e; + } + #calendar-list .brown, + .monthview .timed-events .buble.brown { color: #bd8a36; } + #calendar-list .selected.brown, + .monthview .timed-events .buble.brown.popped-out { background-color: #f7f2e8; } + + /* Green */ + #calendar-list .green label { border-color: #6cb866; } + #calendar .columnview .buble.green .item, + .monthview .untimed-events .buble.green .item { + background-color: #6cb866; + border-color: #4a9b44; + } + #calendar-list .green, + .monthview .timed-events .buble.green { color: #4a9b44; } + #calendar-list .selected.green, + .monthview .timed-events .buble.green.popped-out { background-color: #e6f3e5; } + + /* Blue */ + #calendar-list .blue label { border-color: #7a9ac9; } + #calendar .columnview .buble.blue .item, + .monthview .untimed-events .buble.blue .item { + background-color: #7a9ac9; + border-color: #3c65a3; + } + #calendar-list .blue, + .monthview .timed-events .buble.blue { color: #5f85bf; } + #calendar-list .selected.blue, + .monthview .timed-events .buble.blue.popped-out { background-color: #e4eaf4; } + + /* Indigo */ + #calendar-list .indigo label { border-color: #8e71c5; } + #calendar .columnview .buble.indigo .item, + .monthview .untimed-events .buble.indigo .item { + background-color: #9e81c5; + border-color: #8a67b9; + } + #calendar-list .indigo, + .monthview .timed-events .buble.indigo { color: #8a67b9; } + #calendar-list .selected.indigo, + .monthview .timed-events .buble.indigo.popped-out { background-color: #ebe5f3; } + + /* Violet */ + #calendar-list .violet label { border-color: #ad68b0; } + #calendar .columnview .buble.violet .item, + .monthview .untimed-events .buble.violet .item { + background-color: #ad68b0; + border-color: #904992; + } + #calendar-list .violet, + .monthview .timed-events .buble.violet { color: #ad68b0; } + #calendar-list .selected.violet, + .monthview .timed-events .buble.violet.popped-out { background-color: #f1e5f2; } + + /* (FIXME) Note: IE6 inherits the last class when multiple classes are listed */ + /* See: http://css-discuss.incutio.com/?page=MultipleClasses */ + /* and http://www.quirksmode.org/css/multipleclasses.html */ + /* For now, IE6 users are getting purple highlights. */ + + /* Magic sauce to make text and the bullet the same color */ + .monthview .timed-events .buble .item { color: inherit; } + +/*** Columnview extras (left-side timestrip & background calstripes) ***/ +.timestrip, +.calstripes { + overflow: hidden; + width: 100%; + height: 100%; + /*height: 72em;*/ + position: relative; + z-index: 0; +} + + .timestrip-wrapper { width: 4.5em; text-align: center; } + + .timestrip-wrapper.left, + .timestrip-wrapper.right { + float: none; + } + + .timestrip div { + font-size: 0; + font-weight: normal; + color: #888; + white-space: nowrap; + height: 4.1666667%; /* 100/TotalHours */ + position: relative; + overflow: visible; + } + + .timestrip .spacer { + height: 2.083333333%; + padding: 0; + margin: 0; + } + + .timestrip .even, + .timestrip .odd { + background: url(../img/bg-timestrip.png) no-repeat 100% 50%; + } + + .timestrip-wrapper.right .timestrip .even, + .timestrip-wrapper.right .timestrip .odd { + background: url(../img/bg-timestrip-right.png) no-repeat 0% 50%; + } + + .timestrip div .timeval { + font-size: x-small; + position: absolute; + left: 12px; + right: 12px; + line-height: 1; + margin-top: -0.5em; + top: 50%; + } + + /* IE6 needs a little help with positioning */ + .eng-msie.engv-6 .left .timestrip div .timeval { left: auto; } + + .left .timestrip div .timeval { text-align: right; } + .right .timestrip div .timeval { text-align: left; } + + /* Hide 12-hour (am/pm) by default, and show changes only */ + /* .timestrip div .timeval span { opacity: 0.4; } */ + + .timestrip .first { font-weight: bold; } + .timestrip .first .timeval span { + opacity: 0.75; + font-weight: normal; + } + + .calstripes { } + + .calstripes div, .calstripes td { height: 4.1666667%; /* 100/TotalHours */ } + + /* Calstripe alternative colors */ + .calstripes .odd { background-color: #f5f5ff; } + .calstripes .even { background-color: #fff; } + +/* Styles specific to calendar popups */ + /* Buble rounded corners skin */ + #new-event-popup .popup-buble .t, + #sidebox-calendar .popup-buble .t, + .popup-buble .t { + background-image: url(../img/popup.png); + } + + #new-event-popup .popup-buble .tr, + #sidebox-calendar .popup-buble .tr, + .popup-buble .tr { + background-image: url(../img/popup.png); + } + + #new-event-popup .popup-buble .b, + #sidebox-calendar .popup-buble .b, + .popup-buble .b { + background-image: url(../img/popup.png); + } + + #new-event-popup .popup-buble .br, + #sidebox-calendar .popup-buble .br, + .popup-buble .br { + background-image: url(../img/popup.png); + } + + #new-event-popup .popup-buble .c, + #sidebox-calendar .popup-buble .c, + .popup-buble .c { + background-image: url(../img/popup.png); + } + + #new-event-popup .popup-buble .cr, + #sidebox-calendar .popup-buble .cr, + .popup-buble .cr { + background-image: url(../img/popup.png); + } + + #new-event-popup .popup-buble .t, + #sidebox-calendar .popup-buble .t, + .popup-buble .t { + background-image: url(../img/popup.png); + } + + #new-event-popup .popup-buble .tr, + #sidebox-calendar .popup-buble .tr, + .popup-buble .tr { + background-image: url(../img/popup.png); + } + + #new-event-popup .popup-buble .b, + #sidebox-calendar .popup-buble .b, + .popup-buble .b { + background-image: url(../img/popup.png); + } + + #new-event-popup .popup-buble .br, + #sidebox-calendar .popup-buble .br, + .popup-buble .br { + background-image: url(../img/popup.png); + } + + #new-event-popup .popup-buble .c, + #sidebox-calendar .popup-buble .c, + .popup-buble .c { + background-image: url(../img/popup.png); + } + + #new-event-popup .popup-buble .cr, + #sidebox-calendar .popup-buble .cr, + .popup-buble .cr { + background-image: url(../img/popup.png); + } + + + #info-buble { z-index: 15; } + + #info-bubble .usericon { + /* + width: 64px; + height: 64px; + float: left; + */ + vertical-align: middle; + width: 32px; + height: 32px; + margin: 0 3px 0 0; + } + + .popup-buble .date { white-space: nowrap; } + + .popup-buble .tags { color: #888; } + + .popup-buble .description, + .popup-buble .note { + margin: 1em 0; + } + + .popup-buble #view-apt, + .popup-buble #edit-apt { display: block; text-align: right; } + + .popup-buble #apt-edit * { margin: 2px; } + + .popup-buble .location { color: #888; } + + /* Forms */ + .popup-buble textarea, + .popup-buble select, + .popup-buble input { + font-size: inherit; + font-family: inherit; + } + .popup-buble td.form-label { + text-align: right; + padding-right: 7px; + } + + .popup-buble .date-range-entry.untimed .time-entry-fields { + display: none; + } + + .popup-buble .time-entry-fields td, + .popup-buble .day-entry { + white-space: nowrap; + } + + .popup-buble h3, + #info-summary { + font-weight: bold; + font-size: 110% !important; + /* + min-width: 15em; + width: 15em; + */ + } + + .bublecal { + margin: 0 auto; + } + + #info-summary { + margin-bottom: 3px; + } + + #info-summary, + #info-location { + /* + width: 20em; + max-width: 100%; + */ + } + + .popup-buble textarea { + /* + width: 20em; + max-width: 100%; + */ + } + + #info-tabcontent table { + margin: 6px 0; + } + #info-tabcontent th { + font-weight: normal; + text-align: right; + padding-right: 0.5em; + } + + #info-tabcontent #info-start-time-span, + #info-tabcontent #info-end-time-span { + padding-left: 0.25em; + } + #info-tabcontent #info-start-time-span input, + #info-tabcontent #info-end-time-span input { + margin-left: 0.25em; + } + + #info-tabcontent table td { } + #info-tabcontent table input { } + + #info-content .date { + opacity: 0.5; + } + + .popup-buble .actions, .tz-editor .actions, + .popup-buble .info-actions { + overflow: auto; + text-align: right; + margin-top: 6px; + } + + .eng-msie.engv-6 .popup-buble .actions { + width: 30em; + white-space: nowrap; + } + .eng-msie.engv-6 .popup-buble label { + white-space: nowrap; + } + + .popup-buble .actions .secondary, .tz-editor .actions .secondary { float: left; } + + .popup-buble .actions input, .tz-editor .actions .input, + .popup-buble .actions a, .tz-editor .actions a { + margin: 0 0 0 1em; + text-decoration: none; + } + + .popup-buble .actions input.secondary, + .popup-buble .actions a.secondary, + .popup-buble .actions .secondary input, + .popup-buble .actions .secondary a { + margin: 0 1em 0 0; + } + + .rrule-editor { + margin: 0; + padding: 0; + } + .rrule-editor li { + list-style: none; + padding: 5px; + margin: 0; + } + .rrule-editor label { + display: inline-block; + display: -moz-inline-box; + line-height: 1.8; + vertical-align: top; + width: 75px; + } + .rrule-editor .controls {display: none;} + .rrule-editor.day .controls.day {display: block;} + .rrule-editor.week .controls.week {display: block;} + .rrule-editor.month-by-date .controls.month-by-date {display: block;} + .rrule-editor.month-by-day .controls.month-by-day {display: block;} + .rrule-editor.year .controls.year {display: block;} + .rrule-editor.end-by-date .controls.end-by-date {display: block;} + .rrule-editor.end-by-count .controls.end-by-count {display: block;} + +.icon { height: 16px; width: 16px; } +.icon.web { background-image: url(../img/applications-internet.png); } +.icon.bongo { background-image: url(../img/bongo-favicon.png); } +.icon.readwrite, .icon.publish { background-image: url(../img/accessories-text-editor.png); } +.icon.readonly, .icon.protect { background-image: url(../img/emblem-readonly.png); } +.icon.searchable { background-image: url(../img/system-search.png); } +.icon.info { background-image: url(../img/dialog-information.png); } + +.icon.calendars { background-image: url(../img/icon-simple-calendars.png); } +.icon.addressbook { background-image: url(../img/icon-simple-contacts.png); } +.icon.todos { background-image: url(../img/icon-simple-todos.png); } + +/* Icons for IE6 */ +.icon.web { background-image: url(../img/applications-internet.gif); } +.icon.bongo { background-image: url(../img/bongo-favicon.gif); } +.icon.readwrite, .icon.publish { background-image: url(../img/accessories-text-editor.gif); } +.icon.readonly, .icon.protect { background-image: url(../img/emblem-readonly.gif); } +.icon.searchable { background-image: url(../img/system-search.gif); } +.icon.info { background-image: url(../img/dialog-information.gif); } + +.eng-msie.engv-6 .icon.calendars { background-image: url(../img/icon-simple-calendars.gif); } +.eng-msie.engv-6 .icon.addressbook { background-image: url(../img/icon-simple-contacts.gif); } +.eng-msie.engv-6 .icon.todos { background-image: url(../img/icon-simple-todos.gif); } + +/* Next and previous links */ +.month-next, +.month-prev { + white-space: nowrap; + font-size: 90%; +} + + td.month-next, + td.month-prev { + vertical-align: middle; + padding: 3px 2em; + } + + a.month-next, + a.month-prev, + td.month-next a, + td.month-prev a { + display: block; + } + + a.month-next:hover, + a.month-prev:hover, + td.month-next a:hover, + td.month-prev a:hover { text-decoration: none; } + + .month-next img, + .month-prev img { + width: 16px; + height: 16px; + background-position: 50% 50%; + background-repeat: no-repeat; + vertical-align: middle; + } + + .month-prev img { background-image: url(../img/go-previous.png); } + .month-next img { background-image: url(../img/go-next.png); } diff --git a/src/www/static/css/composer.css b/src/www/static/css/composer.css new file mode 100644 index 0000000..93d5622 --- /dev/null +++ b/src/www/static/css/composer.css @@ -0,0 +1,116 @@ +.msg-compose { + width: 100%; + font-size: inherit; + padding: 1em; +} + +.msg-compose th { + text-align: right; + width: 1px; + font-weight: normal; + vertical-align: top; + padding: 0.25em 0.25em 0 0; +} + +.msg-compose .to, .msg-compose .cc, +.msg-compose .bcc, .msg-compose .subject, +.msg-compose .body, .msg-compose .from { + width: 99%; + font-family: inherit; + font-size: inherit; +} +.msg-compose .from { + width: auto; + margin-right: 1em; +} +.msg-compose td div.text { + border: 2px inset #ccc; + background-color: #fff; + padding: 5px 1px; +} +.msg-compose td div.text blink { + position: relative; + right: 3px; +} +option.underline { + border-bottom: 1px solid #999; + margin-bottom: 2px; +} +.msg-compose textarea { + min-height: 20em; + bottom: 0; +} +.msg-compose .toolbar { + border: 1px solid #ddd; + background-color: #eee; + padding: 3px; + margin: 0.5em 0 0; +} +.msg-compose .toolbar .split { + padding-right: 1px; + display: inline; +} +.icon, +.icon-plus, +.icon-minus { + width: 24px; height: 24px; + background: transparent no-repeat center; + vertical-align: middle; +} +.icon-plus { + background-image: url(../img/plus.png); +} +.icon-minus { + background-image: url(../img/minus.png); +} + +.msg-compose th.attach .icon, +.msg-compose .toolbar .icon { + border: 1px solid transparent; + /*padding: 2px; + display: block; + float: left;*/ +} +.msg-compose .toolbar .icon.selected { + opacity: 0.9; + background-color: #d4e0ec; + border-color: #c2d2e4; +} +.msg-compose .toolbar .icon:hover { + opacity: 0.9; + background-color: #d4e0ec; + border-color: #c2d2e4; +} +.msg-compose tr.attach a img { + border: 0; + text-align: left; + vertical-align: middle; + margin-right: 1px; +} +.msg-compose tr.attach input { + width: auto; + display: inline; +} + +.space-l { + margin-left: 5px; +} +.space-r { + margin-right: 5px; +} + +.attachment { + margin: 0.25em 0; + min-height: 1.5em; +} + +.msg-compose tr.action { + text-align: right; +} +.msg-compose tr.action input { + width: auto; +} + +.msg-compose tr.action input { + margin-left: 3pt; +} diff --git a/src/www/static/css/contacts.css b/src/www/static/css/contacts.css new file mode 100644 index 0000000..7d17e6b --- /dev/null +++ b/src/www/static/css/contacts.css @@ -0,0 +1,163 @@ +#contact-list { + height: 25em; + max-height: 25em; + overflow: -moz-scrollbars-vertical; + overflow-y: auto; + overflow-x: hidden; +} + +#contact-list a { +} + +#contact-view { + width: 100%; + height: 100%; + vertical-align: top; +} + +#contact-view table { + margin: 18px; +} + +#contact-view td { + padding: 0 0 3pt 0; +} +#contact-view td input { + margin-right: 3pt; +} + +td.contact-pic { + text-align: right; + vertical-align: middle; +} + +#contact-pic { + margin-right: 14px; +} + +#contact-title, +#contact-company, +#contact-name { + color: #000; + margin: 0; +/* height: 100%;*/ +} + +.contact-field th { + text-align: right; + color: #999; + text-transform: lowercase; + min-width: 10em; + display: block; +} + +.contact-field .type-label { + -moz-border-radius: 25% 0 0 25%; + border: 1px solid transparent; + padding: 0 12px 0 10px; +} + +#contact-view.editing .semicolon { + visibility: hidden; +} + +#contact-view.editing .type-label { + background: transparent url(../img/arrow.png) right no-repeat; +} + +#contact-view.editing .type-label:hover { + background: transparent url(../img/arrow.png) right no-repeat; + background-color: #e4ebf3; + /* color: #f1f5f9; */ + border-color: #d8e1ed; + text-decoration: none; + cursor: default; +} + +#contact-view.editing .contact-field.empty input, +#contact-view.editing .contact-field.empty textarea { + color: #bbb; +} + +#contact-view.editing .contact-field.empty input:focus, +#contact-view.editing .contact-field.empty textarea:focus { + color: #000; +} + +.contact-field .value { + padding: 2px; + /*display: block;*/ + /*float: left;*/ +} + +.contact-field .sub-label { + margin-left: 1em; +} + +/* hidey bits */ +.contact-field input, +.contact-field textarea, +.contact-field select, +#contact-view.editing .contact-field .sub-label, +#contact-view.editing .contact-field .value { + display: none; +} + +/* showey bits */ +#contact-view.editing .contact-field select, +#contact-view.editing .contact-field input, +#contact-view.editing .contact-field textarea { + display: inline; +} + +.contact-field input, +.contact-field textarea { + border: 1px solid transparent; +} +.contact-field textarea { + width: 100%; + overflow: hidden; +} + +.contact-field select, +.contact-field input:focus, +.contact-field textarea:focus { + border: 1px solid #999; + color: #000; + background-color: #fff; +} + +.del-field, +.add-field { + width: 16px; + height: 16px; +} + +.contact-field.empty { + display: none; +} + +#contact-view.editing .contact-field.empty { + display: table-row; +} + +#contact-view.editing .contact-field .del-field { + background-image: url(../img/minus.png); + visibility: visible; +} + +#contact-view.editing .contact-field .add-field { + background-image: url(../img/plus.png); + visibility: hidden; +} + +#contact-view.editing .contact-field.empty.only .del-field { + visibility: hidden; +} + +#contact-view.editing .contact-field.filled.last .add-field { + visibility: visible; +} + +.contact-field.last td { +} diff --git a/src/www/static/css/conversation.css b/src/www/static/css/conversation.css new file mode 100644 index 0000000..9a483ae --- /dev/null +++ b/src/www/static/css/conversation.css @@ -0,0 +1,394 @@ +/* Conversation navigation */ +.conv-nav { + white-space: nowrap; + width: 100%; +} + + .conv-nav td { vertical-align: middle; } + + .conv-nav a { + padding: 0 1em; + font-size: 85%; + } + + .conv-nav .prev, + .conv-nav .next { text-align: right; } + + .conv-nav input { margin-left: 0.25em; } + + .conv-nav .prev a, + .conv-nav .next a { + display: block; + max-width: 100%; + overflow: hidden; + } + .conv-nav .next a { text-align: right; } + .conv-nav .next { position: relative; } + + .eng-webkit .conv-nav input, + .eng-webkit .conv-nav select { + font-size: 10pt !important; + } + +/* Conversation */ +#conv-subject { + font-family: trebuchet ms, bitstream vera sans, sans-serif; + padding: 0; + margin: 0.5em 1em 0.5em; +} +#conv-msg-list { +} + + .msg { + /* + margin: 1em; + border-top: 1px solid #cadae9; + background-color: #d7f6b8; + */ + font-size: 90%; + position: relative; + margin: 0 1em 12px 64px; + } + + .msg .face { + display: block; + background: transparent url(../img/contact-unknown.png) no-repeat; + width: 48px; + height: 48px; + background-position: center; + background-repeat: no-repeat; + position: absolute; + top: 1.5em; + left: -60px; + margin-top: -2px; + } + + .eng-msie.engv-6 .msg .face { left: -58px; } + + .msg .pointer-border { + display: block; + position: absolute; + top: 1.5em; + left: -10px; + margin-top: 9px; + border: 12px solid #999; + border-left-color: transparent; + border-top-color: transparent; + border-bottom-color: transparent; + border-left: 0; + width: 0; + height: 0; + } + .msg .pointer { + display: block; + border: 9px solid white; + border-left-color: transparent; + border-top-color: transparent; + border-bottom-color: transparent; + width: 0; + height: 0; + position: absolute; + top: -9px; + left: -6px; + } + + .eng-msie.engv-6 .msg .pointer-border { display: none; } + + .msg .info { float: right; } + + /* People 'bubles' in the message header */ + .msg .people { + padding: 0px 2px 0px 2px; + white-space: nowrap; + text-decoration: none; + } + + .msg .msg-subject { overflow: hidden; } + + .msg .people:hover { + /*background: transparent url(../img/arrow.png) right no-repeat;*/ + background-color: #e4ebf3; + padding: 0 1px 0 1px; + -moz-border-radius: 25%; + border: 1px solid #d8e1ed; + text-decoration: none; + } + + .msg .people:hover .comma { + visibility: hidden; + } + + .msg .sender { + font-weight: bold; + } + + .msg .date { + display: block; + clear: both; + margin: 0.5em 0 0 12px; + } + + .msg-content { + border: 2px solid #999; + background-color: #999; + } + .msg .msg-header, + .msg .msg-content .msg-body { + padding: 4px 12px; + border: 1px solid #fff; + } + .msg .msg-header { + } + + .msg .msg-header .people-group { padding-left: 12px; } + + .msg .msg-header .recipients { + } + + .msg.closed .msg-header .recipients { display: none; } + + .msg.closed .msg-header .date { + position: absolute; + top: 0; + right: 0.75em; + } + + .msg .msg-content > .msg-header { + /* Hide IE/Win 6 and below */ + background: transparent url(../img/grad.png) repeat-x top; + } + .msg .msg-content .msg-header a { + text-decoration: none; + } + .msg .msg-content .msg-body { + background-color: #fff; + min-height: 18px; + padding-top: 0.5em; + padding-bottom: 0.5em; + } + + /* Row of actions (reply, forward, etc.) for individual messages */ + .msg-actions { + /* + font-size: small; + padding: 0.25em 8px; + */ + } + .msg-actions.bottom { + margin-bottom: 1.5em; + text-align: right; + } + .msg-actions.top { + /*background-color: #fff;*/ + position: relative; + /* + bottom: 1em; + line-height: 1em; + margin-top: -1em; + */ + float: right; + margin-left: 1em; + /* Have at least enough margin to trigger an extra line of reflowing contacts... */ + margin-bottom: 1px; + margin-right: -0.75em; + } + + .eng-msie.engv-6 .msg-actions.top { margin-right: -0.25em; } + + .msg-actions a { + /* + text-decoration: none; + */ + display: block; + float: left; + padding: 0 0.25em; + min-height: 16px; + white-space: nowrap; + } + .msg-actions a.reply { + padding-left: 28px; + background: url(../img/mail-reply-sender-16.png) no-repeat 8px 0; + } + .msg-actions a.replyall { + padding-left: 28px; + background: url(../img/mail-reply-all-16.png) no-repeat 8px 0; + } + .msg-actions a.forward { + padding-left: 28px; + background: url(../img/mail-forward-16.png) no-repeat 8px 0; + } + + .msg-actions a:hover { + text-decoration: underline !important; + } + + /* closed message */ + .msg.closed { + } + .msg.closed .msg-content { + } + .msg.closed .msg-header { + /* + margin: 0; + height: 1.25em; + overflow: hidden; + */ + } + .msg.closed .msg-body { + display: none; + } + .msg.closed .msg-snippet { + /* + display: inline; + */ + display: block; + /* + display: none; + position: absolute; + top: 0; + left: 200px; + padding: 5px; + height: 1em; + overflow: hidden; + */ + opacity: 0.75; + padding-left: 12px; + margin-top: 0.5em; + } + .msg.closed .pointer-border { + display: none; + } + .msg.closed .face { + top: 0; + } + .msg.closed .msg-actions { + display: none; + } + + /* Attachments */ + .msg .msg-attachments { + border: 1px solid #fff; + border-top: 0; + padding: 0.5em 0.25em 0.5em 24px; + } + .msg.closed .msg-attachments { + padding-top: 0.25em; + padding-bottom: 0.25em; + } + .msg .msg-content > .msg-attachments { + /* + background: transparent url(../img/grad.png) repeat-x top; + */ + } + + .msg .msg-attachments b { + font-weight: normal; + } + + .msg .msg-attachments ul, + .msg .msg-attachments li { + display: inline; + list-style: none; + } + .msg .msg-attachments li { padding-left: 0.25em; } + + /* Composer in conversation view */ + .msg .msg-compose { + border: 2px solid #ccc; + border-top-width: 0; + background-color: #f6f6f6; + } + + /* Msg color */ + /* Unread (new) messages */ + .msg .pointer-border { border-right-color: #8fd78b; } + .msg .msg-content { border-color: #8fd78b; background-color: #d2f2cf; } + .msg .msg-header { color: #43923d; } + .msg .msg-attachments { background-color: #e6f3e5; color: #4a9210; } + .msg .msg-compose { background-color: #e6f3e5; border-color: #8fd78b; } + .msg .disclosure, .msg.closed .disclosure { border-color: #8fd78b; } + + /* Read messages */ + .msg.read .pointer-border { border-right-color: #ddd; } + .msg.read .msg-content { border-color: #ddd; background-color: #eee; } + .msg.read .msg-header { color: #777; } + .msg.read .msg-attachments { background-color: #f6f6f6; color: #777; } + .msg.read .msg-compose { background-color: #f6f6f6; border-color: #ddd; } + .msg.read .disclosure, .msg.read.closed .disclosure { border-color: #aaa; } + + /* Sent messages */ + .msg.sent .pointer-border { border-right-color: #ffdb4e; } + .msg.sent .msg-content { border-color: #ffdb4e; background-color: #ffeda6; } + .msg.sent .msg-header { color: #a78600; } + .msg.sent .msg-attachments { background-color: #fff9e1; color: #a78600; } + .msg.sent .msg-compose { background-color: #fff9e1; border-color: #ffdb4e; } + .msg.sent .disclosure, .msg.sent.closed .disclosure { border-color: #ffdb4e; } + + .msg .msg-snippet { + display: none; + } + + .msg .disclosure, + .msg.read .disclosure, + .msg.sent .disclosure { + margin: 5px 0 0 -12px; + float: left; + border-style: solid; + border-width: 5px; + border-bottom-width: 0; + height: 0; + border-right-color: transparent; + border-left-color: transparent; + position: relative; + right: 4px; + } + .msg.closed .disclosure, + .msg.read.closed .disclosure, + .msg.sent.closed .disclosure { + margin: 2px 2px 0 -9px; + border-width: 5px; + border-right-width: 0; + border-top-color: transparent; + border-bottom-color: transparent; + } + + +/* Message body rules, especially for HTML email */ +.msg-body { + /* + font-family: bitstream vera sans mono, monaco, monospace; + */ + overflow: auto; +} + .msg-body ul, + .msg-body ol, + .msg-body li { + margin: 0 0 0 1em; + } + .msg-body ul li { list-style: disc; } + .msg-body ol li { list-style: decimal; } + + /* + .msg-body html, + .msg-body body, + .msg-body table { font-family: sans-serif; } + */ + + /* iTunes message fixup */ + .msg-body table[width="583"] { font-size: 9pt; } + + .msg-body h1 , + .msg-body h2 , + .msg-body h3 , + .msg-body h4 , + .msg-body h5 , + .msg-body h6 { + text-align: left; + color: inherit; + margin: 0.5em 0; + padding: 0; + } + + .msg-body table { } + + .msg-body pre { font-family: bistream vera sans mono,monaco,luxi mono,monospace; } diff --git a/src/www/static/css/layout.css b/src/www/static/css/layout.css new file mode 100644 index 0000000..92f2e95 --- /dev/null +++ b/src/www/static/css/layout.css @@ -0,0 +1,1225 @@ +/* Reset a few things to start with a pretty clean slate */ +html, body, +h1, h2, h3, h4, h5, h6, +div, +ul, ol, li, +dl, dt, dd, +form +{ + margin: 0; padding: 0; +} + +p { + margin: 0.5em 0; +} +input, select, option, table { + font-family: inherit; + font-size: inherit; + /* we probably shouldn't inherit the color without inheriting + background */ + /* color: inherit; */ +} + +h2 { + padding: 1em 1em 0; +} + +/* +table { + border-collapse: collapse; +} +*/ +li { + list-style: none; +} + +/* Disable the click focus ring -- still leaves the tabbed focus ring intact */ +a:active { + outline: none; +} + +td { vertical-align: top; } + +/* Begin the real stuff */ +html, body { overflow: hidden; } + +body { + background-color: #e4eaf4; + color: #000; +} + body.os-x11 { + font-family: lucida grande, myriad, myriad pro, verdana, luxi sans, bitstream vera sans, sans-serif; + font-size: 0.9em; + } + body.os-mac { + font-family: lucida grande, myriad, verdana, luxi sans, bitstream vera sans, sans-serif; + font-size: 0.85em; + } + body.os-win { + font-family: tahoma, myriad, sans-serif; + font-size: 0.8em; + } + body.os-win.eng-msie.engv-6 { + /* IE6 doesn't have ClearType font smoothing on by default; other browsers on Windows do, including IE7 */ + font-family: arial, tahoma, myriad, sans-serif; + } + +#page-header, +#section-user, +#notification, +#searchtext, +#section-sidebar, +#sidebox-sourcelist, +#sidebox-actions, +#sidebox-summary, +#section-content, +#toolbar, +#searchbar, +#section-list, +#section-itemview, +#sidebox-upcoming { + /* + border: 1px solid red; + */ +} + +a, +a * { + /* One cursor for everyone but Win/IE, the other for only Win/IE */ + cursor: pointer; + cursor: hand; +} +a { + text-decoration: none; +} + +a img { + border: 0; +} + +#content { } + +#page-header td.header-cell { + border-top: 4px solid #ffac35; +} + +a { + color: #517faf; +} +:visited { + color: #a98ba6; + color: #517faf; +} +:active { +} +a:hover { + text-decoration: underline; + color: #f57900; +} + +/* We should not have in Bongo, but just to be sure... */ +a[name] { + color: inherit; + text-decoration: inherit; + cursor: inherit; +} + +.right { + float: right; +} + +#page-header { + /* Position is relative so that children will treat the container as the region for absolute */ + position: relative; +} +#page-header td { + vertical-align: middle; +} + +#logo { + vertical-align: middle; + padding: 10px 15px 10px; +} + +#user-name { + font-weight: bold; + font-size: 1.3em; + letter-spacing: -1px; + color: #fff; +} + +#section-user { + color: #999; + font-size: x-small; +} + #section-user ul, + #section-user li { + display: inline; + list-style: none; + } + #section-user li a { + margin: 0 0 0 1ex; + color: #e4eaf4; + border-bottom: 1px dotted #7a9ac9; + } + #section-user li a:hover { + text-decoration: none; + border-bottom: 1px dotted #e4eaf4; + } + +#top-gradient { + width: 288px; + background: transparent repeat-y center; + vertical-align: middle; + background-image: url(../img/top-gradient.png); +} +#notification { + left: auto; + top: 1.25em; + left: 80ex; + font-size: 85%; + background-color: #ad7fa8; + color: #fff; + padding: 0.5em 1em; + position: absolute; + display: none; + /* This should really be on top */ + z-index: 101; +} +#notification.error { background-color: #f00; } + +#search { + padding-bottom: 0.25em; + margin: 1ex 0; + white-space: nowrap; + font-size: 86%; + padding: 0 1ex; +} + #search-entry { + width: 28ex; + margin-top: 3px; + } + #search label { color: #555; } + + /* Hide the search buttons for Safari */ + body.eng-webkit #search input[type="button"] { display: none; } + +#page-frame { + background: #1d3980 url(../img/login-bg-bottom.png) repeat-x 0 0; + background: #e7e4ef url(../img/bg-page.png) repeat-x 0 0; + width: 100%; + height: 100%; +} +body.eng-msie #page-frame { + position: absolute; + top: 0; + left: 0; +} + +#page-frame td { + /* + border: 1px solid red; + */ +} + +.right-spacer { + /* + width: 1em; + */ +} + +#section-sidebar, +#section-content { + /* Each section */ + vertical-align: top; + height: 100%; + position: relative; +} + + #section-sidebar { + width: 14.5em; + } + #sidebar-nav { + min-width: 120px; + margin: 0 0 0 12px; + padding: 0px 0 15px; + z-index: 5; + position: relative; + background-color: #fff; + border: 1px solid #5f85bf; + border-width: 1px 0 1px 1px; + } + + #sidebar-tabs-small, + .low-res #sidebar-tabs-large { + display: none; + } + + #sidebar-tabs-large, + .low-res #sidebar-tabs-small { + display: block; + } + + #sidebar-tabs-small { + margin-top: 1ex; + margin-left: 1ex; + } + + /* Sidebar rounded corners skin */ + /* + #sidebar-nav .t { background-image: url(../img/side-tl.png); } + #sidebar-nav .tr { background-image: url(../img/side-tr.png); } + #sidebar-nav .b { background-image: url(../img/side-bl.png); } + #sidebar-nav .br { background-image: url(../img/side-br.png); } + #sidebar-nav .c { background-image: url(../img/side-cl.png); } + #sidebar-nav .cr { background-image: url(../img/side-cr.png); } + */ + + #sidebar-nav .action { + padding: 0 0 0 2.5em; + font-size: 85%; + } + + #sidebar-nav .tab { + padding: 1px 0; + letter-spacing: 0; + font-size: 100%; + text-align: left; + border-width: 0; + margin: 0.75em 0 0.25em; + -moz-border-radius: 0; + } + + #sidebar-nav .tab a { + display: block; + font-size: 95%; + padding: 6px 12px 6px 30px; + background-repeat: no-repeat; + background-position: 10px 50%; + min-height: 14px; + font-weight: bold; + text-decoration: underline; + } + + /* IE needs hasLayout for link block hovers */ + .eng-msie #sidebar-nav .tab a { width: 100%; } + + #sidebar-nav .tab a:hover { + background-color: #f2f5fa; + } + + #sidebar-nav .tab.selected { + padding: 0; + border: 1px solid #95a9df; + border-bottom-color: #5f85bf; + border-width: 1px 0; + background: #bbceea url(../img/bg-selected.png) repeat-x 50% 50%; + } + #sidebar-nav .tab.selected a { + padding-top: 5px; + border-top: 1px solid #cfe5fb; + color: #1d3f70; + text-decoration: none; + } + #sidebar-nav .tab.selected a strong { + background-image: none; + } + + #sidebar-nav .tab.selected a:hover { + background-color: transparent; + text-decoration: none; + } + + #sidebox-calendar, + #sidebox-sidebook { margin: 12px; margin-bottom: 0; } + #sidebox-sidebook { padding-bottom: 12px; } + + #sidebox-calendar { } + + #sidebox-calendar .month-widget { + background-color: #f6f6f6; + border: 1px solid #888; + width: 100%; + } + + #sidebox-calendar .month-widget th { + } + #sidebox-calendar .month-widget td { + font-size: 86%; + width: 14%; + text-align: center; + } + + #sidebox-calendar .month-widget a { + display: block; + padding: 1px; + } + #sidebox-calendar .month-widget a:link, + #sidebox-calendar .month-widget a:visited { + /* + color: #777; + */ + } + #sidebox-calendar .month-widget a:active { + outline: none; + } + + #sidebox-calendar .month-widget a.today { + background-image: url(../img/cal-today.png); + background-color: #fff; + border: 1px solid #ffba57; + color: #ee8d03; + padding: 0; + } + + #sidebox-calendar .month-widget a.visible { + background-color: #d1e9cf; + color: #4e9a48; + } + + #sidebox-calendar .month-widget a:hover { + background-color: #c7d4e7; + color: #41679f; + border: 1px solid #b7c8e1; + padding: 0; + text-decoration: none; + } + + #sidebox-calendar .prev, + #sidebox-calendar .header, + #sidebox-calendar .next { + background: #e6e6e6 url(../img/bg-side-title.png) repeat-x 50% 50%; + border-bottom: 1px solid #ccc; + } + + #sidebox-calendar .header { + color: #555; + font-size: 0.85em; + font-weight: normal; + white-space: nowrap; + } + + #sidebox-calendar .prev { border-left-width: 1px; } + + #sidebox-calendar td.prev, #sidebox-calendar td.next { + padding: 0; + vertical-align: top; + } + #sidebox-calendar td.next { + text-align: right; + } + #sidebox-calendar td.next a { + float: right; + } + #sidebox-calendar td.prev a, #sidebox-calendar td.next a { + background-position: center; + background-repeat: no-repeat; + color: transparent; + font-size: 0; + margin: 1px; + height: 20px; + width: 100%; + overflow: hidden; + text-indent: -2000px; + display: block; + } + #sidebox-calendar td.prev a { + background-image: url(../img/go-previous.png) + } + #sidebox-calendar td.next a { + background-image: url(../img/go-next.png); + } + + #sidebox-calendar .todaylink p { + margin: 0; + padding: 0; + } + #sidebox-calendar .todaylink a { + margin: 0 2px 2px; + } + + #sidebox-calendar .daylabels td { + padding: 4px 0 2px; + background-color: #fff; + border-bottom: 1px solid #ddd; + color: #777; + } + + /* Add a 1px space under the day labels */ + #sidebox-calendar .daylabels + tr td { + padding-top: 1px; + } + + #sidebox-calendar a { } + + #adbk { + margin-top: 3em; + background-color: #fefefe; + padding: 2px; + border: 1px solid #ddd; + border-width: 1px 2px 2px 1px; + overflow: hidden; + font-size: 90%; + } + + #adbk-card { + overflow: auto; + } + + #adbk-header td { + vertical-align: top; + } + + #adbk-search{ + width: 95%; + } + + #adbk-img { + } + + #adbk-card spacer { + min-height: 1em; + height: 1em; + } + + #adbk-card td { + white-space: nowrap; + } + + #adbk-card th { + text-align: right; + overflow: hidden; + white-space: nowrap; + text-transform: lowercase; + } + + #adbk-card th:after { + content: ":" + } + + /* For some strange reason, IE6 wants to compute width differently + * ...unless a negative right margin is set */ + .eng-msie.engv-6 .list-selector .search-field, + .eng-msie.engv-6 .list-selector .unselected-items { margin-right: -5%; } + + #section-content { + border: 1px solid #5f85bf; + border-width: 1px 0 0 1px; + } + + +#sourcebar, +#toolbar { + background: #fcfcfc url(../img/bg-toolbar.png) repeat-x 0 100%; + border-bottom: 1px solid #bbb; + color: #999; + padding: 6px 12px; + font-size: 90%; + clear: both; +} +#sourcebar { + padding: 3px 12px; +} + +#sourcebar + #toolbar { +} + +#toolbar .conv-nav { + border-collapse: collapse; + padding: 0; + margin: 0; +} +#toolbar .conv-nav td { + padding: 0; +} + +#toolbar input { + /* + margin: 0.5em 0.25em; + */ +} + +#toolbar .spacer { + margin: 0 0.25em; +} + +#toolbar .bar { + color: #ccc; + display: none; +} + +#toolbar td { font-size: 88%; } + +/* Add a little spacing around some buttons in the mail toolbar */ +#toolbar #mail-toolbar-archive, +#toolbar #mail-toolbar-star { + margin-left: 2.5ex; + margin-right: 2.5ex; +} + +#sourcebar { + overflow: auto; +} + +#sourcebar li { + display: inline; + margin: 0 0.5ex; +} + +#sourcebar li a, +#toolbar a { + padding: 5px 6px; +} + +#sourcebar li.selected a, +#toolbar a.selected { + background-color: #999; + border: 1px solid #444; + padding: 0 5px; + color: #fff; +} + +#toolbar .next a, +#toolbar .prev a { + padding: 0; +} + +#sourcebar a { + float: left; + display: block; + padding: 6px 8px; +} +#sourcebar li.selected a { + padding: 4px 6px; +} + +.eng-msie.engv-6 #sourcebar { padding: 0.7em; } +.eng-msie.engv-6 #sourcebar a { display: inline; float: none; } + +#sourcebar li a:hover, +#toolbar a:hover { + cursor: pointer; + cursor: hand; + text-decoration: none; +} + +#quickevent { + width: 15em; +} + +.sidebox { + width: 12.5em; +} + + +#section-upcoming h3, +#section-summary h3 { + margin-top: 1em; +} + +#section-summary li.selected { + background-color: #517faf; + +} +#section-summary li.selected a { + color: #fff; +} + +#section-content { +} +#section-content { + background-color: #fff; + margin-bottom: 0; +} + +#searchbar { + background-color: #feefda; + color: #fa9504; + font-weight: bold; + font-size: larger; + position: relative; + border-bottom: 1px solid #fa9504; + padding: 0.25em 0.5em; +} + +.conversation-list .time { + /*text-align: right;*/ + text-align: right; +} +.conversation-list .date { + /*width: 10em;*/ +} +.conversation-list .subject { + overflow: hidden; +} + +#item-list-nav { + text-align: right; + display: none; +} + +.pager { + text-align: center; + margin: 2em; +} + +#searchbar .button-close { + display: block; + position: absolute; + top: 0.5em; + right: 1em; +} +#section-list { + background-color: #fff; +} +#section-itemview { + /* + border: 1px solid #7ed12d; + */ + border-left: 0; + border-right: 0; + background: #fff; + overflow: hidden; + height: 99%; + padding: 0; + margin: 0; +} + +#section-upcoming { + display: none; +} + +#searchbar { +} + +.tab-space { + width: 100%; + border-bottom: 1px solid #fffdfa; +} + +#ifolder-tab { + display: none +} + +#summary-tab { margin-top: 0.5em !important; } + +/* Tab icons */ +#summary-tab-href { background-image: url(../img/home-16.png); } +#mail-tab-href { background-image: url(../img/message-16.png); } +#calendar-tab-href { background-image: url(../img/calendar-16.png); } +#addressbook-tab-href { background-image: url(../img/contact-16.png); } + +/* Again, for IE6 */ +.eng-msie.engv-6 #summary-tab-href { background-image: url(../img/home-16.gif); } +.eng-msie.engv-6 #mail-tab-href { background-image: url(../img/message-16.gif); } +.eng-msie.engv-6 #calendar-tab-href { background-image: url(../img/calendar-16.gif); } +.eng-msie.engv-6 #addressbook-tab-href { background-image: url(../img/contact-16.gif); } + +#search-tab { + width: 100%; + margin-right: -1px; + text-align: left; +} + +#search-tab label { color: #555; } + +#search-tab a { + padding: 0; + display: inline; +} + +.sidebox { + font-size: 90%; + border: 1px solid; + margin: 0 0.5em 1.5em 0.5em; + background-color: #fff; + color: #000; +} +.sidebox a { +} + +.sidebox .title { + color: #fff; + font-size: x-small; + text-transform: uppercase; + letter-spacing: 3px; + text-align: center; + padding: 1px 2px; + font-weight: normal; +} + +.sidebox .body { + padding: 0.5em 0.5em; +} + +.sidebox li { + white-space: nowrap; + overflow: hidden; + display: block; +} + +.sidebox li.selected { + background-color: #7ed12d; + background: #7bc828 url(../img/navselhilight.png) repeat-x top; +} + +.sidebox li.selected a { + color: #fff; +} + +.sidebox li input { + margin: 0 0.5em; +} + +.sidebox li a { +} + +#sidebox-bookmarks .title { + border-color: #fa9504; + background-color: #fa9504; +} + +#sidebox-bookmarks .body { + border-color: #fa9504; +} + +#bookmark-commands { + margin-top: 6px; +} + +#remove-bookmarks { + display: block; + visibility: hidden; +} + +#add-bookmark { + display: block; + float: right; +} + +#sidebox-sourcelist .add-source { + text-align: right; + padding-right: 0.5em; +} + +#sidebox-actions .title { + border-color: #ae7cad; + background-color: #82bf51; +} + +#sidebox-actions .body { + border-color: #ae7cad; + padding-left: 1em; +} + +#sidebox-sourcelist .title { + border-color: #93d94c; + /* + */ + background-color: #93d94c; + background-color: #9f7cc8; + background-color: #937dad; +} + +#sidebox-sourcelist .body { + border-color: #94d94c; + padding: 0; +} + +#sidebox-sourcelist li a { + display: block; + padding: 0.25em 0.5em; + color: #76539f; +} +#sidebox-sourcelist li.selected a, +#sidebox-sourcelist li a.selected { + /* + background-color: #7ed12d; + background: #7bc828 url(../img/navselhilight.png) repeat-x top; + */ + background-color: #dfd3ed; +} +#sidebox-sourcelist li a:hover { + text-decoration: none; + background-color: #f8f2f8; +} +#sidebox-sourcelist li.selected a:hover, +#sidebox-sourcelist li a.selected:hover { + background-color: #d9c8e6; +} +#sidebox-sourcelist li a img { + vertical-align: bottom; + margin-right: 3px; +} + +#sidebox-sourcelist #calendar-list { padding: 0.375em 0; } + +#sidebox-upcoming .title { + border-color: #8f5902; + /* + background-color: #e9b96e; + */ + background-color: #8f5902; +} + +#sidebox-upcoming .body { + border-color: #8f5902; +} + +/* Misc. Development stuff */ +#_MockiKit_LoggingPane { + z-index: 101; +} +#interpreter_form { + background-color: Window; + color: WindowText; + border-bottom: 1px solid ThreeDShadow; + position: relative; + z-index: 102; + font-size: 12pt; +} +#interpreter_area { + background-color: #fff; + color: #000; +} + +.left { float: left; } +.right { float: right; } + +/*** Forms style ***/ +input { + font-family: inherit; + font-size: inherit; + /* we probably shouldn't inherit the color without inheriting + background? */ + /* color: inherit; */ +} + +input, +textarea { + padding: 1px; +} +input[type="text"], +input[type="search"], +input[type="password"], +input:not([type]), +button, +textarea { + border: 1px solid #bbbed0; + padding: 2px; + background: #fff url(../img/grad-textarea.png) repeat-x top; +} +input[type="text"]:focus, +input[type="search"]:focus, +input[type="password"]:focus, +input:not([type]):focus, +button:focus, +textarea:focus { + /* + border: 2px solid Highlight; + padding: 1px; + */ +} +input[type="text"]:hover, +input[type="search"]:hover, +input[type="password"]:hover, +input:not([type]):hover, +button:hover, +textarea:hover { + background-color: #fffdf9; +} + +input.empty { color: #999; } + +.text-entry { + border: 1px solid #bbb; +} + +/* Due to IE6's braindeadness, we cannot combine this with CSS that includes [type=""], as below */ +.toggle-button { + -moz-border-radius: 2px; + background-image: url(../img/bg-button.png); + background-position: bottom; + background-repeat: repeat-x; + font-weight: normal; + padding: 1px 3pt !important; + border-width: 0; +} +.widgetcolor, +.toggle-button { + background-color: #fbf6e3; + color: #3c2b14; + -moz-border-top-colors: #ce9d59 #fdf8f2 transparent; + -moz-border-right-colors: #ce9d59 #fcf7f0 transparent; + -moz-border-bottom-colors: #ce9d59 #fcf7f0 transparent; + -moz-border-left-colors: #ce9d59 #fdf8f2 transparent; + padding: 2pt; +} + +/* The comment above applies here, plus IE6 applies .toggle-button.selected + to all .selected unless it's specifically specified, as below */ +.toggle-button-group .toggle-button.selected { + background-image: url(../img/bg-button-selected.png); + background-color: #ffd69b; +} + +/* Hacks to make buttons look better in Mozilla-based browsers */ +body.os-x11.eng-gecko input[type="submit"], +body.os-x11.eng-gecko input[type="reset"], +body.os-x11.eng-gecko input[type="button"], +body.os-x11.eng-gecko button { + -moz-border-radius: 2px; + background-image: url(../img/bg-button.png); + background-position: bottom; + background-repeat: repeat-x; + font-weight: normal; + padding: 1px 3pt !important; + border-width: 2px; +} + +body.os-x11.eng-gecko input[type="submit"], +body.os-x11.eng-gecko input[type="reset"], +body.os-x11.eng-gecko input[type="button"], +body.os-x11.eng-gecko button { + background-color: #fbf6e3; + color: #3c2b14; + -moz-border-top-colors: #ce9d59 #fdf8f2 transparent; + -moz-border-right-colors: #ce9d59 #fcf7f0 transparent; + -moz-border-bottom-colors: #ce9d59 #fcf7f0 transparent; + -moz-border-left-colors: #ce9d59 #fdf8f2 transparent; +} +body.os-x11.eng-gecko input[type="submit"]:hover, +body.os-x11.eng-gecko input[type="reset"]:hover, +body.os-x11.eng-gecko input[type="button"]:hover, +body.os-x11.eng-gecko button:hover { + background-color: #fff; +} +body.os-x11.eng-gecko input[type="submit"]:focus, +body.os-x11.eng-gecko input[type="reset"]:focus, +body.os-x11.eng-gecko input[type="button"]:focus, +body.os-x11.eng-gecko button:focus { + -moz-border-top-colors: Highlight Highlight transparent; + -moz-border-right-colors: Highlight Highlight transparent; + -moz-border-bottom-colors: Highlight Highlight transparent; + -moz-border-left-colors: Highlight Highlight transparent; +} +body.os-x11.eng-gecko input[type="submit"]:active, +body.os-x11.eng-gecko input[type="reset"]:active, +body.os-x11.eng-gecko input[type="button"]:active, +body.os-x11.eng-gecko button:active { + background-image: url(../img/bg-button-pressed.png); + background-position: top; + -moz-border-top-colors: #ce9d59 #fcf7f0 transparent; + -moz-border-right-colors: #ce9d59 #fcf7f0 transparent; + -moz-border-bottom-colors: #ce9d59 #fcf7f0 transparent; + -moz-border-left-colors: #ce9d59 #fcf7f0 transparent; +} +body.os-x11.eng-gecko input[type="submit"][disabled], +body.os-x11.eng-gecko input[type="reset"][disabled], +body.os-x11.eng-gecko input[type="button"][disabled], +body.os-x11.eng-gecko button[disabled] { + -moz-border-top-colors: #ce9d59 #fdf8f2 transparent; + -moz-border-right-colors: #ce9d59 #fcf7f0 transparent; + -moz-border-bottom-colors: #ce9d59 #fcf7f0 transparent; + -moz-border-left-colors: #ce9d59 #fdf8f2 transparent; +} + +body.os-x11.eng-gecko input[disabled], +body.os-x11.eng-gecko input[disabled]:hover, +body.os-x11.eng-gecko input[disabled]:active, +body.os-x11.eng-gecko select[disabled], +body.os-x11.eng-gecko select[disabled]:hover, +body.os-x11.eng-gecko select[disabled]:active, +body.os-x11.eng-gecko textarea[disabled], +body.os-x11.eng-gecko textarea[disabled]:hover, +body.os-x11.eng-gecko textarea[disabled]:active { + background-image: none; + background-color: #f4f4f4; + border-color: #bbb; + color: #999; + opacity: 0.75; +} +body.os-x11.eng-gecko input[type="button"][disabled], +body.os-x11.eng-gecko input[type="button"][disabled]:hover, +body.os-x11.eng-gecko input[type="button"][disabled]:active, +body.os-x11.eng-gecko input[type="reset"][disabled], +body.os-x11.eng-gecko input[type="reset"][disabled]:hover, +body.os-x11.eng-gecko input[type="reset"][disabled]:active, +body.os-x11.eng-gecko input[type="submit"][disabled], +body.os-x11.eng-gecko input[type="submit"][disabled]:hover, +body.os-x11.eng-gecko input[type="submit"][disabled]:active, +body.os-x11.eng-gecko button[disabled], +body.os-x11.eng-gecko button[disabled]:hover, +body.os-x11.eng-gecko button[disabled]:active { + background-image: none; + background-color: #fbf3e2; + color: #bbb; + opacity: 0.75; +} + +body.eng-msie input, +body.eng-msie button, +body.eng-msie select { + font-family: tahoma, arial, helvetica, sans-serif; + font-size: 9pt; +} + +body.os-mac input, +body.os-mac button, +body.os-mac select { + font-family: lucida grande, helvetica, sans-serif; + font-size: 0.95em; +} +body.os-mac.eng-webkit input, +body.os-mac.eng-webkit button, +body.os-mac.eng-webkit select { + font-size: 1em; +} + +input, button, select, textarea { } + +body.os-x11.eng-gecko input[type="checkbox"], +body.os-x11.eng-gecko select { + -moz-border-top-colors: Transparent #c9bdab; + -moz-border-right-colors: White #c9bdab; + -moz-border-bottom-colors: White #c9bdab; + -moz-border-left-colors: Transparent #c9bdab; +} + body.os-x11.eng-gecko input[type="checkbox"]:focus/*, + body.os-x11.eng-gecko select:focus*/ { + -moz-border-top-colors: Highlight Highlight transparent; + -moz-border-right-colors: Highlight Highlight transparent; + -moz-border-bottom-colors: Highlight Highlight transparent; + -moz-border-left-colors: Highlight Highlight transparent; + } + /* Okay, this :hover is a Moz/FF work-around to make single-clicking selects work. */ + /*select:hover { + -moz-border-top-colors: Transparent #c9bdab; + -moz-border-right-colors: White #c9bdab; + -moz-border-bottom-colors: White #c9bdab; + -moz-border-left-colors: Transparent #c9bdab; + }*/ + + body.os-x11.eng-gecko input[type="checkbox"] { + background-image: url(../img/grad-textarea.png); + background-position: top; + background-repeat: repeat-x; + background: #fff url(../img/grad-textarea.png) repeat-x top; + } + +#content-iframe { + width: 100%; +} + +/* Extra developer-centric links below the sidebox */ +#extra { + color: #999; + font-size: 75%; + text-align: center; + line-height: 1.5; + margin: 3ex 0 0; +} +#extra a { + color: #999; +} + +.indented { + margin-left: 16px; +} + +.scrolledheader { + margin-right: 16px; +} + +#sidebox-sidebook { } + + #sidebox-sidebook .notebook-tabs { + background-color: #ccc; + position: static; + border: 1px solid #999; + height: 24px; + overflow: hidden; + position: relative; + } + + #sidebox-sidebook .notebook-tabs a { + margin: 0; + padding: 0; + text-align: center; + border: 0; + float: left; + width: 50%; + background: #e6e6e6 url(../img/bg-side-title.png) repeat-x 50% 50%; + border-top: 1px solid #eee; + } + #sidebox-sidebook .notebook-tabs a:hover { + background: #f6f6f6 url(../img/bg-side-title-hover.png) repeat-x 50% 50%; + } + + #sidebox-sidebook .notebook-tabs a.selected { + margin: 0; + padding: 0; + background: #bbceea url(../img/bg-selected.png) repeat-x 50% 50%; + border-color: #cfe5fb; + } + #sidebox-sidebook .notebook-tabs a img { + display: block; + border: 1px solid #999; + border-width: 0 1px; + width: 100%; + height: 23px; + } + #sidebox-sidebook .notebook-tabs a img.addressbook { border: 1px solid #999; border-width: 0 1px 0 0; } + #sidebox-sidebook .notebook-tabs a img.todo { border: none; } + + .eng-msie.engv-6 #sidebox-sidebook .notebook-tabs a img { border: none; } + .eng-msie.engv-6 #sidebox-sidebook .notebook-tabs a img.calendars { border-width: 0; } + + #sidebox-sidebook .notebook-content { + margin: 0; + background-color: #f6f6f6; + border: 1px solid #999; + } + +.group-footer, +.search-group { + clear: both; +} + +pre { + white-space: pre-wrap; /* CSS3 */ + white-space: -moz-pre-wrap; /* Moz */ + white-space: -o-pre-wrap; /* Opera */ + word-wrap: break-word; /* IE */ +} + +#loading { + font-family: sans-serif; + font-size: 18pt; + text-decoration: blink !important; + position: absolute; + top: 25%; + left: 0; + right: 0; + width: 100%; + text-align: center; +} + +#tzlabel { color: #fff; } + +#search-no-results +{ + padding-top: 25%; +} + +#nomail-summary +{ + padding-top: 50%; + text-align: center; +} + +#preferences-container +{ + padding-right: 2em; + padding-left: 2em; +} diff --git a/src/www/static/css/log.css b/src/www/static/css/log.css new file mode 100644 index 0000000..5f39c30 --- /dev/null +++ b/src/www/static/css/log.css @@ -0,0 +1,106 @@ +/* + * log.css + * Visual themeing for browser-based logging provided by Log.js + * + * (c) 2008 Alexander Hixon + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2, or (at your option) + * any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA + */ + + +#logbox { + display: block; + z-index: 1000; + left: 0px; + bottom: 0px; + position: fixed; + width: 100%; +} + +#log { + font-family: Inconsolata, Dejavu Sans Mono, monospace; + background-color: #2e3436; + color: #eeeeec; + padding: 2px; + height: 100px; + overflow: auto; +} + +#jsprompt { + font-family: Inconsolata, Dejavu Sans Mono, monospace; + background-color: #555753; + color: #eeeeec; + + border: 0px none; + width: 100%; +} + +#log div.error { + color: #ef2929; +} + +#log div.debug { + color: #73d216; +} + +#log div.warn { + color: #f57900; +} + +#log .group div.error { + color: #a40000; +} + +#log a, #log a:active { + text-decoration: none; + border-bottom: 1px dotted #babdb6; + color: #eeeeec; +} + +#log .groupbox a, #log .groupbox a:active { + color: #2e3436; +} + +#log div.a { + color: #eeeeec; +} + +#log a:hover { + border-bottom: 0px none; +} + +#log .titlespan { + background-color: #888a85; + width: 100%; +} + +.group { + padding: 2px; + background-color: #555753; +} + +.groupbox { + margin-top: 0.5em; + margin-bottom: 0.5em; + margin-left: 1em; + border: 1px solid #babdb6; +} + +#log .floatright { + float: right; + color: #eeeeec; + /*padding: 2px; + display: inline;*/ +} diff --git a/src/www/static/css/login.css b/src/www/static/css/login.css new file mode 100644 index 0000000..eb758f6 --- /dev/null +++ b/src/www/static/css/login.css @@ -0,0 +1,92 @@ +body.login { + background-color: #1d3980; + overflow: hidden; +} + + #login-pane { + position: absolute; + top: 25%; + left: 0; + right: 0; + text-align: center; + color: #333; + padding: 1em 0 1em; + border-top: 2px solid #d4dceb; + border-bottom: 2px solid #e4eaf4; + margin-bottom: 298px; + background: #b4c5e1 url(../img/login-bg-middle.png) repeat-x 0 0; + } + + #login-logo { + position: absolute; + top: -76px; + left: 0; + right: 0; + width: 100%; + width: 100%; + font-size: 0; + text-indent: -2000px; + color: transparent; + overflow: hidden; + height: 118px; + display: block; + background: url(../img/login-logo.png) no-repeat 50% 0; + } + .eng-msie.engv-6 #login-logo { background-image: url(../img/login-logo.gif); } + + #login-message { + padding: 3em 0 0.75em; + font-size: 87%; + } + + #login-pane input { + color: #000; + } + + #login-pane table { + text-align: left; + margin: 0 auto; + } + + #login-pane th, + #login-pane td { + text-align: right; + font-weight: normal; + } + + #login-pane td.default { + text-align: left; + } + + #login-pane .text-entry { + width: 15em; + } + + #login-pane h2 { + color: #99b; + margin: 1em; + } + + #login-pane a { + } + + #login-bg-top, + #login-bg-bottom { + position: absolute; + width: 100%; + left: 0; + right: 0; + } + #login-bg-top { + background: url(../img/login-bg-top.png) repeat-x 0 100%; + height: 185px; + top: -188px; + border-bottom: 1px solid #385798; + } + #login-bg-bottom { + background: url(../img/login-bg-bottom.png) repeat-x 0 0; + height: 293px; + bottom: -296px; + border-top: 1px solid #1d3980; + } + .eng-msie.engv-6 #login-bg-bottom { bottom: -298px; } diff --git a/src/www/static/css/print.css b/src/www/static/css/print.css new file mode 100644 index 0000000..a74eba3 --- /dev/null +++ b/src/www/static/css/print.css @@ -0,0 +1,201 @@ +body, +#content { + background: none; + border: none; + font-size: 10pt; + font-family: times new roman, times, serif !important; + /* + display: inline; + */ +} + +/* Turn off UI chrome */ +#notification, +#section-user, +#section-sidebar, +#sourcebar, +#toolbar, +#tabs-and-search, +#conv-breadcrumbs, +#page-header +{ display: none !important; } + +#page-frame #section-content, +#page-frame td td { display: table-cell; } + +#content-iframe { display: block; } + +/* Summary */ + +/* Mail */ +.msg.closed { display: none; } + +#conv-msg-list { padding: 0; } + +.msg { + border-top: 2px dashed #000; + margin-top: 2em; + page-break-before: auto; + page-break-after: auto; + page-break-inside: avoid; +} + + .msg .msg-content { border: none; } + + .msg .msg-header { + background: none !important; + page-break-inside: avoid; + display: block; + margin-bottom: 1em; + } + + .msg .msg-header .sender { + padding: 0.25em 0; + margin: 0; + font-size: 150%; + display: block; + position: relative; + right: 10px; + font-weight: bold; + } + + .msg .msg-header .disclosure { + display: none; + } + .msg .msg-header .date { + display: block; + } + + .msg .msg-header .people { + padding: 0 1ex 0 0; + } + + .msg .pointer-border { + display: none; + } + + .msg .face { + top: 0; + margin: 2px; + border: 1px dashed #ccc; + /* Hide for now */ + display: none; + } + + .msg .msg-body { + clear: both; + display: inline; + line-height: 1.4; + font-size: 10pt; + page-break-before: avoid; + page-break-after: auto; + page-break-inside: avoid; + } + + .msg .msg-actions, + .msg .msg-snippet { + display: none; + } + + +/* Calendar */ +#calendar { } + + #calendar .buble { + overflow: visible; + font-size: 8pt; + } + + .timestrip, + .calstripes, + table.columnview ul.timed-events { + height: 8.25in; + } + + /* Calendar items - generic */ + #calendar .item { + -moz-border-radius: 0; + background: none !important; + } + + #calendar .columnview { } + + #calendar .columnview .item { + overflow: visible; + margin: 0; + padding: 0; + } + + #calendar .columnview .item strong { + color: #000; + background-color: #fff; + padding: 2px; + text-align: left; + margin: 2px 2px 2px 2px; + } + + #calendar .columnview .item .resize { + display: none; + } + + /* Calendar items - timed */ + #calendar .columnview .timed-events .item { + border-left: 10px solid red; + } + + /* Calendar items - untimed */ + #calendar .columnview .untimed-events .buble { + } + #calendar .columnview .untimed-events .item { + height: auto; + border-bottom: 10px solid red; + } + #calendar .columnview .untimed-events .item strong { + padding: 1px; + } + #calendar .columnview .untimed-events .item .note { + background-color: #fff; + color: #000; + font-size: 90%; + } + + .calstripes div { + border-top: 1px dotted #999; + /* Magic to make the lines not count for space */ + margin-top: -1px; + position: relative; + top: 1px; + } + + .timeval span { + } + +.scroll, +.scrollv, +.scrollh { + overflow: visible !important; + height: auto !important; + width: auto !important; + max-height: auto !important; +} + +/* +body { + overflow: auto !important; +} + +.conv-msg-list * { + position: static !important; +} +*/ + +pre { + font-family: bitstream vera sans mono, monaco, monospace; + font-size: 10pt; +} + + + +#conv-subject { + font-size: xx-large; +} diff --git a/src/www/static/css/summary.css b/src/www/static/css/summary.css new file mode 100644 index 0000000..2b81bba --- /dev/null +++ b/src/www/static/css/summary.css @@ -0,0 +1,139 @@ +#summary-table { + width: 100%; +} + +td.summary-column { + vertical-align: top; + width: 50%; +} + +.summary-category { + margin: 1em 1em 2em 1em; +} + + .summary-category h3 { + text-transform: uppercase; + letter-spacing: 1px; + font-weight: normal; + font-size: x-small; + color: #555; + border-bottom: 1px solid #ccc; + padding: 1px; + } + + /* Generic summary items */ + .summary-category li { clear: both; } + + .summary-category li a { padding: 0.5em 0.75em; } + .summary-category li a:hover { + color: #f50; + text-decoration: none; + } + .summary-category #frommycontacts-list li a:hover, + .summary-category #subscriptions-list li a:hover { + text-decoration: underline; + } + + .summary-category li a span { display: block; } + + .summary-category li.unread a { + font-weight: bold; + } + + /* Stars (and other icons, if they're going to exist) */ + .summary-category li a img { + float: left; + margin-right: 0.5em; + } + + /* Truncate multiple-row events and mail subjects */ + .summary-category.cal-summary li a span.summary-event, + .summary-category li a span.summary-subject { + line-height: 1.5em; + height: 1.5em; + overflow: hidden; + position: relative; + } + + /* Mail summary items */ + .summary-category.mail-summary a { + font-size: 85%; + display: block; + } + .summary-category li a span.summary-from { + color: #999; + font-weight: normal; + } + .summary-category li.even a span.summary-from { + /* + color: #8d99a4; + */ + } + + .summary-category li a span.summary-subject { + font-size: 120%; + } + .summary-category li a:hover span.summary-subject { + text-decoration: underline; + } + + /* Stars, etc. */ + .summary-category.mail-summary li a img { + margin-bottom: 0.5em; + } + + /* Calendar summary items */ + .summary-category.cal-summary li a { + display: block; + padding: 0.25em 0.75em; + font-size: 85%; + clear: both; + overflow: auto; + text-align: right; + } + .summary-category.cal-summary #today-list li a span.summary-event { + font-weight: bold; + } + .summary-category.cal-summary li a span.summary-event { + font-size: 120%; + float: left; + } + .summary-category.cal-summary li a:hover span.summary-event { + text-decoration: underline; + } + + .summary-category.cal-summary li a span.summary-day, + .summary-category.cal-summary li a span.summary-time { + color: #555; + display: inline; + white-space: nowrap; + } + .summary-category.cal-summary li a span.summary-day { + /*float: right;*/ + } + .summary-category.cal-summary #later-list li a span.summary-time { + padding-right: 0.75em; + color: #999; + } + +/*** Icons ***/ +.summary-category h3 { + background: transparent url(../img/message-closed-16.png) no-repeat 0 50%; + padding-left: 20px; + padding-top: 4px; +} +.cal-summary h3 { + background-image: url(../img/event-16.png); +} + +#summary-cal { + margin-top: 1em; +} +#summary-cal .date { + font-size: 90%; + text-decoration: none; +} +#summary-cal .date a:hover { + color: #f50; + text-decoration: underline; +} diff --git a/src/www/static/css/widgets.css b/src/www/static/css/widgets.css new file mode 100644 index 0000000..76026e4 --- /dev/null +++ b/src/www/static/css/widgets.css @@ -0,0 +1,568 @@ +.conversation-list { + width: 100%; + font-size: inherit; +} + +/* Force the date and time to 1 line high */ +.conversation-list .date, +.conversation-list .time { white-space: nowrap; } + +/* Eeevil hack to work around the scrollbar eating dates + * issue in IE6 (okay, perhaps it isn't that evil) */ +.eng-msie.engv-6 .conversation-list .date { padding-right: 22px; } + +.conversation-list table { + width: 100%; +} + +.conversation-list tr.unread { + font-weight: bold; +} +.conversation-list td { + color: #517faf; + padding: 0.25em 0 0.25em 0.5em; + vertical-align: top; + font-size: 86%; +} +.conversation-list td.time { + padding-right: 0.5em; +} + +.summary-category.mail-summary a, +.conversation-list tr td.subject a { color: #a38bad; } + +.summary-category .unread a, +.conversation-list tr.unread td.subject a { color: #3974ce; } + +.summary-category a .summary-from, +.conversation-list tr td.from a, +.conversation-list tr:hover td.from a { + color: #777; + text-decoration: none; +} +.summary-category .unread a .summary-from, +.conversation-list tr.unread td.from a { + color: #000; +} + +.conversation-list tr td.date a, +.conversation-list tr td.time a, +.conversation-list tr:hover td.date a, +.conversation-list tr:hover td.time a { + color: #777; + font-weight: normal; + text-decoration: none; +} + +.conversation-list tr:hover td.from a, +.conversation-list tr:hover td.subject a { + text-decoration: underline; + color: #f57900; +} + +.conversation-list .group-header td { + font-size: 105%; + color: #555; + background-color: #f6f6f6; + border: 1px solid #e2e2e2; + border-width: 1px 0; + padding: 1ex 2ex; +} +.conversation-list .group-footer .empty { + color: #aaa; + padding: 0 4ex; +} + +.even, +.conversation-list tr.even { + background-color: #f5f8fb; +} + +/* Constrain checkbox and star widths to minimum */ +.conversation-list td.checkbox, +.conversation-list td.star { width: 1%; } + +.conversation-list div.star { background-position: 0 0; } +.conversation-list div.star.starred { background-position: 0 -32px; } +.conversation-list div.star:hover { background-position: 0 -16px; } +.conversation-list div.star:focus, +.star:hover, +.star:focus { + background-position: 0 -32px; + text-decoration: none; +} +div.star { + float: left; + display: block; + list-style:none; + margin: 0 6px 0 0; + padding:0px; + width: 16px; + height: 16px; + background: url(../img/star-widget.png) 0 -16px no-repeat; + text-indent: -1000em; +} +/* for IE6 only */ +.eng-msie.engv-6 div.star { background-image: url(../img/star-widget.gif); } + +hr.hresize, +hr.vresize { + border: 0; + padding: 2px; + cursor: move; +} +hr.hresize { + border-width: 0 1px; +} +hr.vresize { + border-width: 1px 0; +} + +/* Rounded corners widgets */ +.t, .tr, .c, .cr, .b, .br { + display: block; + z-index: -1; + background-repeat: no-repeat; +} + .t { + /* + height: 100%; + */ + position: absolute; + top: 0; left: 0; right: 0; + bottom: 0; + margin: 0 24px 24px 0; + background-position: top left; + } + .tr { + margin: 0 0 24px 0; + position: absolute; + right: -24px; + top: 0; + height: 100%; + width: 24px; + background-position: top right; + } + .b { + margin: 0 24px 0 0; + position: absolute; + bottom: 0; left: 0; right: 0; + height: 24px; + background-position: bottom left; + } + .br { + z-index: 22; + position: absolute; + bottom: 0; + right: -24px; + width: 24px; + height: 24px; + background-position: bottom right; + } + .c { + display: none; + height: auto; + position: absolute; + top: 24px; bottom: 24px; left: 0; right: 0; + background-repeat: repeat-y; + background-position: left; + } + .cr { + display: none; + margin-right: -24px; + height: auto; + position: absolute; + top: 0; right: 0; bottom: 0; left: 0; + background-repeat: repeat-y; + background-position: right; + } + +/* Define a scrollable region -- just needs the size (height and/or width) set */ +.scroll, +.scrollv, +.scrollh { + position: relative; + overflow: auto; +} +/* Fix for IE */ +.eng-msie.engv-6 .scroll, .eng-msie.engv-6 .scrollv, .eng-msie.engv-6 .scrollh { width: 100%; } + + .scrollv { + overflow-x: hidden; + overflow-y: auto; + } + .scrollh { + overflow-x: auto; + overflow-y: hidden; + } + +#section-sidebar .notebook { font-size: 87%; } + +.notebook-tabs { + margin-top: 1ex; + z-index: 5; + position: relative; + top: 2px; + overflow: auto; +} + .notebook-tabs a { + display: block; + float: left; + margin: 1px; + padding: 0.25ex 1.5ex 0.5ex; + } + .notebook-tabs a.selected { + margin: 0; + border: 1px solid #d6ae12; + border-bottom: none; + background-color: #fffcf4; + outline: none; + color: #4f420e; + text-decoration: none; + cursor: default; + } + +/* IE6 has problems with the tab floats */ +.eng-msie.engv-6 .notebook-content { + margin-top: -8px; + position: relative; + clear: both; +} + +.notebook.tabs-visible .notebook-content { + padding: 1.5ex; + margin-bottom: 1ex; + border: 1px solid #d6ae12; + background-color: #fffcf4; +} + + .autocompletions ul { + list-style-type: none; + background: #fff; + border: 1px solid ThreeDDarkShadow; + margin: 0; + padding: 0; + overflow: auto; + overflow-x: hidden; + overflow-y: auto; + /* Line height * lines = max-height (8 lines * 1.5 = 12em) */ + /* Autocomplete scrolls after 8 lines, in other words */ + max-height: 12em; + } + + .autocompletions li { + white-space: nowrap; + list-style-type: none; + margin: 0; + padding: 1px; + display: block; + } + + .autocompletions li.selected { + /* Native system highlight colors */ + background: Highlight; + color: HighlightText; + } + +/* toggle buttons */ +.toggle-button-group { + text-align: center; + border-collapse: collapse; +} + +.toggle-button-group td { + border: 1px solid #ce9d59; + padding: 0; +} + +.toggle-button-group td.spacer { + border: none; +} +.toggle-button-group td a { + display: block; +} +.toggle-button-group td a:hover { + text-decoration: none; +} +.toggle-button { + display: block; +} + +/* list selector */ +.list-selector { margin-right: 0.5em; } + + .list-selector .search-field { + width: 100%; + margin-bottom: 1ex; + } + + .list-selector .unselected-items { + width: 100%; + background-color: white; + overflow: auto; + overflow-y: auto; + overflow-x: auto; + border: 1px solid #bbbed0; + position: relative; + } + + .list-selector li { + padding: 3px 5px; + cursor: default; + line-height: 1; + white-space: nowrap; + position: relative; + overflow: hidden; + } + .eng-msie.engv-6 .list-selector li { + white-space: normal; + position: static; + } + + .list-selector li.popped-out { background-color: #e1e9f3; } + + .list-selector.searching .unselected-items li {display: none;} + .list-selector.searching .unselected-items li.hit {display: block;} + + .list-selector li input { + vertical-align: middle; + } + .list-selector li .icon { + /* + position: relative; + z-index: 5; + */ + } + .list-selector li .cal-name { + white-space: nowrap; + overflow: hidden; + line-height: 1.5; + position: absolute; + left: 22px; right: 24px; + } + + /* IE6 is going to have to wrap, at least for now. */ + .eng-msie.engv-6 .list-selector li .cal-name { + white-space: normal; + position: static; + overflow: display; + } + + +/*** The Popup Buble ***/ +.popup-container { + position: absolute; + left: 0; + top: 0; + visibility: hidden; +} + + .popup-content p { + overflow: auto; + max-width: 10em; + } + .calendar-popup .popup-content p { + overflow: visible; + max-width: none; + } + + .popup-content .description { + max-width: 700px; + max-height: 250px; + overflow: auto; + } + .eng-msie .popup-content .description { + width: 35em; + height: 10em; + } + + .popup-container .pointer { + position: absolute; + display: block; + width: 25px; + height: 25px; + background-repeat: no-repeat; + } + + .popup-container .pointer.above { + background-image: url(../img/info-pointer-down.png); + bottom: -3px; + left: -8px; + } + + .popup-container .pointer.below { + background-image: url(../img/info-pointer-up.png); + top: -3px; + left: -8px; + } + + .popup-container .pointer.right { + background-image: url(../img/info-pointer-left.png); + top: -8px; + left: -3px; + } + + .popup-container .pointer.left { + background-image: url(../img/info-pointer-right.png); + top: -8px; + right: -3px; + } + + .eng-msie.engv-6 .popup-container .pointer.above { background-image: url(../img/info-pointer-down.gif); } + .eng-msie.engv-6 .popup-container .pointer.below { background-image: url(../img/info-pointer-up.gif); } + .eng-msie.engv-6 .popup-container .pointer.right{ background-image: url(../img/info-pointer-left.gif); } + .eng-msie.engv-6 .popup-container .pointer.left { background-image: url(../img/info-pointer-right.gif); } + +.popup-buble { + position: relative; + font-size: 0.9em; + line-height: 1.4em; + min-height: 48px; + min-width: 125px; + padding: 8px 14px 12px 10px; +} + + .popup-buble.closable .occurrence-entry, + .popup-buble.closable h3 { margin-right: 26px; } + + .popup-buble h2 { + margin: 0; + padding: 0; + font-size: 1.2em; + display: inline; + } + + .popup-buble h3 { padding-bottom: 10px; } + + .popup-buble p { max-width: 25em; } + + .popup-buble td div.date { display: none; } + .popup-buble .calendar { background-color: white; } + + /* (X) graphic */ + .popup-buble .close { + display: block; + position: absolute; + overflow: hidden; + top: 8px; + right: 12px; + width: 20px; + height: 20px; + background: url(../img/x.png) 0 0; + cursor: pointer; cursor: hand; + } + + .popup-buble .close:hover { + background-position: 20px 0; + } + + .popup-buble .pointer { + width: 25px; + height: 25px; + display: block; + position: absolute; + background-repeat: no-repeat; + } + .popup-buble .pointer.right { + background-image: url(../img/info-pointer-right.png); + right: -20px; + } + .popup-buble .pointer.left { + background-image: url(../img/info-pointer-left.png); + left: -23px; + } + .popup-buble .pointer.up { + background-image: url(../img/info-pointer-up.png); + top: -23px; + } + .popup-buble .pointer.down { + background-image: url(../img/info-pointer-down.png); + bottom: -20px; + } + + .eng-msie.engv-6 .popup-buble .pointer.right { background-image: url(../img/info-pointer-right.gif); } + .eng-msie.engv-6 .popup-buble .pointer.left { background-image: url(../img/info-pointer-left.gif); } + .eng-msie.engv-6 .popup-buble .pointer.up { background-image: url(../img/info-pointer-up.gif); } + .eng-msie.engv-6 .popup-buble .pointer.down { background-image: url(../img/info-pointer-down.gif); } + +/* popup-buble IE6 hacks */ +.eng-msie.engv-6 .t, .eng-msie.engv-6 .tr, .eng-msie.engv-6 .c, .eng-msie.engv-6 .cr, .eng-msie.engv-6 .b, .eng-msie.engv-6 .br { display: none; } +.eng-msie.engv-6 .popup-buble { + background-color: #fffbe9; + border: 1px solid #c8a000; + margin: 3px 4px 4px 3px; + width: 16em; +} +.eng-msie.engv-6 .popup-buble .form-list li { white-space: nowrap; } +.eng-msie.engv-6 .popup-buble .form-list li label { margin-right: 1ex; } + +.form-list { + padding: 5px; +} + +.form-list li { + padding-top: 7px; +} + +.form-list label { + display: inline-block; + line-height: 1.8; + text-align: right; + vertical-align: top; + width: 75px; +} +.eng-gecko .form-list label { display: -moz-inline-box; } + +hr { + border: 1px dotted #ddd; + background-color: #ddd; + border-width: 0 0 1px 0; + padding: 0; + height: 1px; +} +.popup-buble hr { + border-color: #c8a000; + background-color: #c8a000; +} + +.action, +.actions { white-space: nowrap; } + +.actions { min-width: 20em; } + +.calendar-popup .actions { + min-width: 0; + overflow: visible !important; +} + +.tz-wrapper { + position: relative; + display: inline; +} + +.tz-editor { + position: absolute; + width: 300px; + padding: 6px; + right: 0px; + display: none; + color: black; + background-color: #f6f6f6; + border: 1px solid #bbb; + z-index: 20; +} + + .tz-editor td { + width: 50%; + } + + .tz-editor select { + border-width: 1px; + width: 100%; + } + + .tz-editor th { + padding: 0px; + font-weight: normal; + text-align: left; + } diff --git a/src/www/static/dragonfly.png b/src/www/static/dragonfly.png new file mode 100644 index 0000000000000000000000000000000000000000..8c81499027cb25afbd1aeb5994b8c4f68c922e38 GIT binary patch literal 6971 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 N002ovPDHLkV1gPT*unq+ literal 0 HcmV?d00001 diff --git a/src/www/static/favicon.ico b/src/www/static/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..b65fbceec3bd0573700d4a68ac83a4ed3956aedd GIT binary patch literal 1406 zcmeH{dr;I>6vsb3tSl=m)3PipEA6ShA5sX0!HTHs!mhaBg5t8e>}ng2NI-~3BDC#~ z(6n&|hZ!Jf8pqLmGp;5SG&Vk11vZ9hY|O1?SzLCJUFa_2pZ?`P{n5Gez2|7B% z1(DGS$YOQK<8`PLHN-795Ua`{CN7T{WigAD78WhtMXa(4xq6F;RiRi}fn45*LSrFC zTY@~X0eNyQ>a;4BB{!g0U5{G7jU}4>B&0R4Lf42Xs}xo0LDcC#px#hNe9~{I(hiHf zJ;bfCB2Q^Wx%LDq?J1H>`%tEzM3cRjlG1XK{|joPjk1cbNzAd4ly^qpEXf-?(dODn z$?rm&XJ=K>c~Z;Ugtueb)Q!&ELV9U82Fp28&A(%m+A)^>$+}7#I;n%=Z>{JhJBF$Y z6bP>0dJ#jVgZ0~csH@vU<~JA7Z~vR@ojsU@-%)SHSo0UAYA2bsmnqogBCEztM%_Q; z*7TEA-%C!TnrSiCs9Y1;5+3cgX?F!Yu4sx#DM)j$y)SSLb?U^B3+b_}C z(aHG^JKvoh;zH*iblH1o>+a{W!@(s-FHWZum&-+8UmtFFKOT<CZhh1@y*o=?WoL*IPyjk%9bd+_yZ6=jN7o_p=Bg>OIqNbcyK zTb953(hCvlCGXFal?E%yHMdj zT@XKc&1n7Ru-S`Wo%DEA-wjj@}3mVW`0OoRFW literal 0 HcmV?d00001 diff --git a/src/www/static/img/Bongo-logo.png b/src/www/static/img/Bongo-logo.png new file mode 100755 index 0000000000000000000000000000000000000000..a752adc3986cb85a0812119b500a1eb7a70ccad5 GIT binary patch literal 5439 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 literal 0 HcmV?d00001 diff --git a/src/www/static/img/accessories-text-editor.gif b/src/www/static/img/accessories-text-editor.gif new file mode 100644 index 0000000000000000000000000000000000000000..bfd8c2b923cede6e8c1aa725300c00e8b0a6a816 GIT binary patch literal 401 zcmZ?wbhEHb6krfwSZdAy1U^n`+2Ljd(U$#@Ov%Z~g@uKsrKN3cZ3~*i7PN#fY6)N5 z8rsp-x}-h4r>CdCzyHVrhPiX+o|vC%3QZ@-J#W8m)2yRUt73wA+KVKYs_3G8@*RS8adGq$|+jsBY{r~?TNMZuTf1=Js zsfi`2DGKG8B^e4K8L0~Hp1uJLia%KxxftvjbUVf$M9y)xXf=i5Ms`M!t ztuT>NHB*vZV>Vk=z+pd|u25IanT$i;&K!@kK2-3!Hl&KZW$R0Q_1(5ql8;3_uc$an zjI+h5s-~VrPlStw*SV_Dghf}lnQdBcwVEnBvjh)|0LOwhQ$rR;CKe%0@il#_N(?Nl o{3_Zq+w)C~jSY{Om>!&NE_3d@jP$uRqP*9x-{2K>a%8Xu05y5RG5`Po literal 0 HcmV?d00001 diff --git a/src/www/static/img/accessories-text-editor.png b/src/www/static/img/accessories-text-editor.png new file mode 100644 index 0000000000000000000000000000000000000000..3155f7ed3fdfda9b594159c2737049cbdf7ac090 GIT binary patch literal 439 zcmV;o0Z9IdP)w2ftik_5!$BL) zXrthAiG*sA7SY+g2=8g}hF9f!qqDpR&UxSO`M$$}f3BFn5klMpTR;cc1Kxpspbc0+ zYpey^wi|&IrzEpN2(7i&QPe5PtnW3(ssLQqC5rm@Cj*dDqLd<&$)J?NvaGo(rJS9C zQ^xhrM2JVL<;hPU-^IJ}h1zTa`dSGMS8Q{&HIYL?$rG z>43RA08eujiD0^NcL2a;`hns?f_atS5(gaJ)QdAM%p>R%G^ehgd1^F)x&~sQ1V%Z{ zP8CkRKRI3jFIQoGa~Eg=ECHiXf_N2P4q@ZWW;L-r4L6!;!8Fal4Xt(9s~Ujk2h?uw h?A#xIaSiYQegL2ys-?IHV~hX*002ovPDHLkV1nG!u+{(o literal 0 HcmV?d00001 diff --git a/src/www/static/img/addressbook-16.png b/src/www/static/img/addressbook-16.png new file mode 100644 index 0000000000000000000000000000000000000000..b64b42fb5cc67310fc9b2446199c54172fcc7dad GIT binary patch literal 776 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 literal 0 HcmV?d00001 diff --git a/src/www/static/img/agent-bongoavirus.png b/src/www/static/img/agent-bongoavirus.png new file mode 100644 index 0000000000000000000000000000000000000000..8714aa228f292100d388240ed83538f3f0ee9a4b GIT binary patch literal 2866 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 Q%K!iX07*qoM6N<$f+=}8V*mgE literal 0 HcmV?d00001 diff --git a/src/www/static/img/agent-bongocalcmd.png b/src/www/static/img/agent-bongocalcmd.png new file mode 100644 index 0000000000000000000000000000000000000000..2e3a4b0efe6bf1ed14dcf2f24262e62e8cff666f GIT binary patch literal 2017 zcmV<72Oju|P)_mwo>CBA&5sdNXaIFJfztrVVMOuj*;XlI@ZgZicsGftNf2X8zN6CZuDVtAedm1VaeGKraW@Y>Y5-q!+#_(*;~s&d9u)vQ``hzB zfYNi;HYbi6&|!u#W=u>!{K=1h`Vv$Xzy8hn-GhSzy<5g?wy(c>_7}UV>OEj>b7Ey> z1>4($#8Ma3hyFQ5e_8*WLV?e9Zt?RUe)v&1=S~1YLOH-U|MCVluW$Ci-wET?0}tYf z#~<&UtI9>2*EjL%>gx~@5CH%oOvPtED1M>tbK+N`_Chpf@?@GaGbbl#_cI7jJ^4K} zO|zUpe6Xs5iF%-Ouq`YCF@cD{6lUg@L`ER$K>>&=R23=;Re^{k8iby@JQ>w4*<}QX z3edTc10cif!5RZY1Y!m=h4r3DZvFfn{t&1FP6R5RfT(&RBFReB4~yjll1($<5i|v2 zYzz!Dm>Fysh7CB7NnYRJC~J2>ZoTM>wJnMV}4zssOr=m92bEdYR+VVGfz z@dvXfVi-*RGXY@3$$Lc9laH5xe~-8pya$<326Jm+t@Qz%-Q(p5q(Huv zgfzIUswz~bLUpX_0gs@)qzu@2B1COwO7#NXdLn|RX)xX2$7F90)4eHXvsr7;yr9Lj zsjaP3TJPg8VnF@kDgdn#;B1bvN<>VEgWwI~^sv!r2cw-4#^Z4SoLHt#+)DWC0b*5M_Nn*JtOo)%Vu5t6`O$>&^PT9|cdKu_=ilET4H|Y>sLO??R zMLo0eDKilX^?=!cnS%8h^Z5Y=pAI^p2>^3m2fDA$@l$+v0lFp9UOSY8IOKQ33?|BQ ze5|;B-FQ64U@*X>o}?GFJybE`>5K?bAJDS#nG0ELCn%)>1qcvv7;f)kXEZ`%ZKr7E zebdsB`WuaRlqyH-69PfK-sli9kS?aQ)h12YsuA%C?#4hlD)3+2ve8bYLllvm*iwMl*vz#%#J9-}QRZ{^K z%w`Ry)2X+30$UQuGE7Mz|FQt^vf%Zng2X~U7UQy9+RH*J3g;a5rc4qO>k|lE#f;ZBkmX%n0wS=^Vmh6sekQkk4nC2If!>?@qn~vaONN;I zK7%OqT+sstr3-q^oOG-nOc(JNy?*?BSOQB&Of*1h*P~>ZWD3ZUKQJ zPb9;~zufX!y3O_vQCcW$)fTO?IJN1ND-$fwEdkRKA!4kruVZa(4S;^sClL1oIf*#s z`bHtIM5Y@X>0T|q7Q{wcm(X{$4j@^#l}vkl`AaKcW<2u9qj>0{2XXeyS)4q15^rC;Fx%eU zd5ehX77{`iX!a@R(Mg=0!0yIE#IGSea_YGZh^eFb*n8c7|8e~Cne(XkYHV(Pib-8# z{om_S*ORRmUwH8!0C=H<7L!PTu-4+zrN8fg{GW}dS6^AZ79J@%gs_H~>ynr6-}szV%r4t#h$^ zPSIbt^js)VS=UMg-)*M);;+q{Io`i=S8{6^44&B``xw6@4s{5!n>RB)^6az@2RSb z_fRM%Cjflqh;Zj4_R9)D;I$qlGY7D*MXrxAu!lS2AAapm8lHe4SYDT;Mas63$NnD( zZ!==e4tuHF=W@E$?~ZrFxD#aag~mMsM?L-v-KTfQ&I=JX00000NkvXXu0mjf__MSO literal 0 HcmV?d00001 diff --git a/src/www/static/img/agent-bongocollector.png b/src/www/static/img/agent-bongocollector.png new file mode 100644 index 0000000000000000000000000000000000000000..2e3a4b0efe6bf1ed14dcf2f24262e62e8cff666f GIT binary patch literal 2017 zcmV<72Oju|P)_mwo>CBA&5sdNXaIFJfztrVVMOuj*;XlI@ZgZicsGftNf2X8zN6CZuDVtAedm1VaeGKraW@Y>Y5-q!+#_(*;~s&d9u)vQ``hzB zfYNi;HYbi6&|!u#W=u>!{K=1h`Vv$Xzy8hn-GhSzy<5g?wy(c>_7}UV>OEj>b7Ey> z1>4($#8Ma3hyFQ5e_8*WLV?e9Zt?RUe)v&1=S~1YLOH-U|MCVluW$Ci-wET?0}tYf z#~<&UtI9>2*EjL%>gx~@5CH%oOvPtED1M>tbK+N`_Chpf@?@GaGbbl#_cI7jJ^4K} zO|zUpe6Xs5iF%-Ouq`YCF@cD{6lUg@L`ER$K>>&=R23=;Re^{k8iby@JQ>w4*<}QX z3edTc10cif!5RZY1Y!m=h4r3DZvFfn{t&1FP6R5RfT(&RBFReB4~yjll1($<5i|v2 zYzz!Dm>Fysh7CB7NnYRJC~J2>ZoTM>wJnMV}4zssOr=m92bEdYR+VVGfz z@dvXfVi-*RGXY@3$$Lc9laH5xe~-8pya$<326Jm+t@Qz%-Q(p5q(Huv zgfzIUswz~bLUpX_0gs@)qzu@2B1COwO7#NXdLn|RX)xX2$7F90)4eHXvsr7;yr9Lj zsjaP3TJPg8VnF@kDgdn#;B1bvN<>VEgWwI~^sv!r2cw-4#^Z4SoLHt#+)DWC0b*5M_Nn*JtOo)%Vu5t6`O$>&^PT9|cdKu_=ilET4H|Y>sLO??R zMLo0eDKilX^?=!cnS%8h^Z5Y=pAI^p2>^3m2fDA$@l$+v0lFp9UOSY8IOKQ33?|BQ ze5|;B-FQ64U@*X>o}?GFJybE`>5K?bAJDS#nG0ELCn%)>1qcvv7;f)kXEZ`%ZKr7E zebdsB`WuaRlqyH-69PfK-sli9kS?aQ)h12YsuA%C?#4hlD)3+2ve8bYLllvm*iwMl*vz#%#J9-}QRZ{^K z%w`Ry)2X+30$UQuGE7Mz|FQt^vf%Zng2X~U7UQy9+RH*J3g;a5rc4qO>k|lE#f;ZBkmX%n0wS=^Vmh6sekQkk4nC2If!>?@qn~vaONN;I zK7%OqT+sstr3-q^oOG-nOc(JNy?*?BSOQB&Of*1h*P~>ZWD3ZUKQJ zPb9;~zufX!y3O_vQCcW$)fTO?IJN1ND-$fwEdkRKA!4kruVZa(4S;^sClL1oIf*#s z`bHtIM5Y@X>0T|q7Q{wcm(X{$4j@^#l}vkl`AaKcW<2u9qj>0{2XXeyS)4q15^rC;Fx%eU zd5ehX77{`iX!a@R(Mg=0!0yIE#IGSea_YGZh^eFb*n8c7|8e~Cne(XkYHV(Pib-8# z{om_S*ORRmUwH8!0C=H<7L!PTu-4+zrN8fg{GW}dS6^AZ79J@%gs_H~>ynr6-}szV%r4t#h$^ zPSIbt^js)VS=UMg-)*M);;+q{Io`i=S8{6^44&B``xw6@4s{5!n>RB)^6az@2RSb z_fRM%Cjflqh;Zj4_R9)D;I$qlGY7D*MXrxAu!lS2AAapm8lHe4SYDT;Mas63$NnD( zZ!==e4tuHF=W@E$?~ZrFxD#aag~mMsM?L-v-KTfQ&I=JX00000NkvXXu0mjf__MSO literal 0 HcmV?d00001 diff --git a/src/www/static/img/agent-bongoimap.png b/src/www/static/img/agent-bongoimap.png new file mode 100644 index 0000000000000000000000000000000000000000..57ac1fc187d84abbe521464bb83d5e0fbfa7ec86 GIT binary patch literal 3421 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 literal 0 HcmV?d00001 diff --git a/src/www/static/img/agent-bongomailprox.png b/src/www/static/img/agent-bongomailprox.png new file mode 100644 index 0000000000000000000000000000000000000000..eec6a3c654a523b105a4a67f30f31c0dd1f6cbac GIT binary patch literal 3165 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 literal 0 HcmV?d00001 diff --git a/src/www/static/img/agent-bongopop3.png b/src/www/static/img/agent-bongopop3.png new file mode 100644 index 0000000000000000000000000000000000000000..57ac1fc187d84abbe521464bb83d5e0fbfa7ec86 GIT binary patch literal 3421 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 literal 0 HcmV?d00001 diff --git a/src/www/static/img/agent-bongoqueue.png b/src/www/static/img/agent-bongoqueue.png new file mode 100644 index 0000000000000000000000000000000000000000..2e3a4b0efe6bf1ed14dcf2f24262e62e8cff666f GIT binary patch literal 2017 zcmV<72Oju|P)_mwo>CBA&5sdNXaIFJfztrVVMOuj*;XlI@ZgZicsGftNf2X8zN6CZuDVtAedm1VaeGKraW@Y>Y5-q!+#_(*;~s&d9u)vQ``hzB zfYNi;HYbi6&|!u#W=u>!{K=1h`Vv$Xzy8hn-GhSzy<5g?wy(c>_7}UV>OEj>b7Ey> z1>4($#8Ma3hyFQ5e_8*WLV?e9Zt?RUe)v&1=S~1YLOH-U|MCVluW$Ci-wET?0}tYf z#~<&UtI9>2*EjL%>gx~@5CH%oOvPtED1M>tbK+N`_Chpf@?@GaGbbl#_cI7jJ^4K} zO|zUpe6Xs5iF%-Ouq`YCF@cD{6lUg@L`ER$K>>&=R23=;Re^{k8iby@JQ>w4*<}QX z3edTc10cif!5RZY1Y!m=h4r3DZvFfn{t&1FP6R5RfT(&RBFReB4~yjll1($<5i|v2 zYzz!Dm>Fysh7CB7NnYRJC~J2>ZoTM>wJnMV}4zssOr=m92bEdYR+VVGfz z@dvXfVi-*RGXY@3$$Lc9laH5xe~-8pya$<326Jm+t@Qz%-Q(p5q(Huv zgfzIUswz~bLUpX_0gs@)qzu@2B1COwO7#NXdLn|RX)xX2$7F90)4eHXvsr7;yr9Lj zsjaP3TJPg8VnF@kDgdn#;B1bvN<>VEgWwI~^sv!r2cw-4#^Z4SoLHt#+)DWC0b*5M_Nn*JtOo)%Vu5t6`O$>&^PT9|cdKu_=ilET4H|Y>sLO??R zMLo0eDKilX^?=!cnS%8h^Z5Y=pAI^p2>^3m2fDA$@l$+v0lFp9UOSY8IOKQ33?|BQ ze5|;B-FQ64U@*X>o}?GFJybE`>5K?bAJDS#nG0ELCn%)>1qcvv7;f)kXEZ`%ZKr7E zebdsB`WuaRlqyH-69PfK-sli9kS?aQ)h12YsuA%C?#4hlD)3+2ve8bYLllvm*iwMl*vz#%#J9-}QRZ{^K z%w`Ry)2X+30$UQuGE7Mz|FQt^vf%Zng2X~U7UQy9+RH*J3g;a5rc4qO>k|lE#f;ZBkmX%n0wS=^Vmh6sekQkk4nC2If!>?@qn~vaONN;I zK7%OqT+sstr3-q^oOG-nOc(JNy?*?BSOQB&Of*1h*P~>ZWD3ZUKQJ zPb9;~zufX!y3O_vQCcW$)fTO?IJN1ND-$fwEdkRKA!4kruVZa(4S;^sClL1oIf*#s z`bHtIM5Y@X>0T|q7Q{wcm(X{$4j@^#l}vkl`AaKcW<2u9qj>0{2XXeyS)4q15^rC;Fx%eU zd5ehX77{`iX!a@R(Mg=0!0yIE#IGSea_YGZh^eFb*n8c7|8e~Cne(XkYHV(Pib-8# z{om_S*ORRmUwH8!0C=H<7L!PTu-4+zrN8fg{GW}dS6^AZ79J@%gs_H~>ynr6-}szV%r4t#h$^ zPSIbt^js)VS=UMg-)*M);;+q{Io`i=S8{6^44&B``xw6@4s{5!n>RB)^6az@2RSb z_fRM%Cjflqh;Zj4_R9)D;I$qlGY7D*MXrxAu!lS2AAapm8lHe4SYDT;Mas63$NnD( zZ!==e4tuHF=W@E$?~ZrFxD#aag~mMsM?L-v-KTfQ&I=JX00000NkvXXu0mjf__MSO literal 0 HcmV?d00001 diff --git a/src/www/static/img/agent-bongosmtp.png b/src/www/static/img/agent-bongosmtp.png new file mode 100644 index 0000000000000000000000000000000000000000..9655f1ef4e066ed6effb3263e8505eee94079ab0 GIT binary patch literal 2338 zcmV+-3ElRIP)7a(C`2Og1-cm%;qEG~8u@9x~zj&O)gEN3IjS$mzmcrSge z;-R{Gs%LuEJ4V>Dlp~$#KGi)n-#Jy^Ij5&dN{J^~*y9H9zsjc$JZ>@427mjzvmXaR zFdEsM^Tom3;`qwy=t2+#qmSB-F#PC;uRgUS2ZA6NedWhL0TCc2P_548YW1p9$WPhD zV)>EV0C4V&-;AbVg_N>QUqAb6DW#Ch%lG8k+NunKKuRfO5Cn2_bK{ZQLI|lawPQ_P z0p%FLxdGqzu)4YoB0`jMZF}pH*}2>hs?|B0bH18}GnK@g^Tld)&hG2$Ppnky%Kd+B zfQW%HhFZO94dxcd)3Bzlz+i51yjHI|rBXSL#6R9!#aq|bK+#PU9jL|bEVSK4P0ACy z@YSO^01`q76!JxT{&Hghz|Kg#vO2m@$WPfqNGpoW%m-`u$`?mqFqQHT~7+Ls0a0FGm$SS(w$deut1zhf3E7R#{ht$2(PA!Bf?YQ2ruB2ef75rHv=YOP|83@_zU z?@t>HIbboSCLmIi8Ju7m1j8U027|#FOic|QT~qg&2EpX%YIIZv6(fR_fDi)3LdjlP z9bMS51BwGhTL>9v2SxxVg2@;R0Bd(Y0{KomIF_OvoG@DgU~;v>Y8BMB^gI`ZLdnuQ z16>}#0XrUxR)8}C;~crc5nO)fPgwtC348W^9=pd5;HmL1fE)Uhq|o^*q2 z9e9fuCSw@rH89X)AP9i9Ef4p%d=(&ylEQ;I(6J6wYZa$hEQ2vs2MB6;IU`6kv(DJ~ z1jfcEuyKDGjf;Q9r8ob8z2iqveBmYZ^z2GasryEHad5N`oF%>5aD05Uv;`KYAyo%T z#c8|VsHWC|jvOc!O12Op<^U1ExkhA-bS7sQ92~*(FTRXZU;i%d-C4l-KfaFl=P$sw zHxi^hwm*l%WBqO50iZ8y;K;s#IQjs=^IYWfh3+{}D3qYK55-xCg**sdYTw%g96FlE z-u;L0=imPd|El~Q?=>zUH}n|{j*Q~K{{5I3ItC1krTuI(s+x1AnL)i?dw34GuA?R; zz~lsvHy178g0@xMjde&7pr?Nqj+dUpy}P&Y@vWQq@cO&B__x2{*=J7U#c#g^!$|KX z2}urAsG(X*u@|aT=Pbq;nlU9X1{hJ#!zoOw zswXm7sGNa^YG}$B;r>>7C)+BdwGu)IOqWk|-wTzBWm||SY7|ytiaXLsIYV-c^lMEu z7-40@gD(VxklU)>PW!%(si}PTbs*w^E}z`@u<7_(q~f!L-O3H!zP)T?JL$8yR*3<_ zFwkf;x~~KEdM!6KmA5?4RX7;I@qBDKK?0QSL|j0lL&6~~%WTu#nVjNw8c-aVox9Zi z^FXCKt8qY0<_8i^5Woq9QX!h>K@i(+c+Cwo6-qtPKIUp;o)C$YDulp^>61<;4s25g zinb8Kf@FVU#QKaQwg^ z=5MZG`p^ij-@YHY4_}D*fd(KLhKc$4y2CkNNyFN)4%BKj+ceF@xEBaGUI5$k;dnuK z9>nL47r^n=(e*`mRAa6e;KJ2ASljgQx!paGQo{2EB!ICY3+2PZaQy&YkgOL(gqhh( z*@vhDiUUzTj#dF-l}w&DJ66)Z5VauDc=_-MK0DHj#T6Sj7B^5hIE)YPu47?o6L0@} zN!`~{%cYb!F@36g4wOp~2a-Y^j`6vK#)$}py7<=ByG@P&xV^j;|ACymxhaYRQ`wo> zceowOjv#8hW?%8u^ z&}cMV)6AgJXmnwlDRwkpz0J;KvaqZk{oO$?fUJ&_^cM?6WC5_wy>aH65OPmAw)EQ1 ze|Gw{U;Of)Qp(+r+Safwz52>e%K&TuTL5e+rQPZP2ViMlasbW#`SFPfMGP-g2g-qf zT3_vi1j#`0g8^{y_$3=21E_$yG6de{z>_9V{G0ku9eCXGUt%!6bPx3AJpcdz07*qo IM6N<$fkwc;aAC$_c1LS#5@A+%M>2vVZUD)v zlTNzx?7TXCZ@=z6`$y7lCM1xDRn}JdtDL&2)a~Ez^Etmh_ny8T!a2wP=McVbfT_ke z0$+E0Bk*+x8_!84rNsxSK$eAqp(G-Z0420X)AiFKu9fHK zUE?{Q-0Ct~P*7knTMhLOFI^U&m36le3I?J2RkXO8wR7h_@Q1@%na``fvUBIo@KoVn zf4yKDVW!Jm=dvCSW^dj4TK$v-0HCO-C{bX%>TjNSGCn1Fw$R~eht+%=C}lW*zS4j7 z+SN~!5@&yJ_3G8yWZ~a<;|)2i`7hX)KKw1S(JZ`s_Sc?JK+oR3eS5>rUN^2aux;D6 z7ClU|-~G+6+nQUPdR+80bab|%x6gy6_T`2-bLQON)ZDOdvhV;%Nb{e*_ujc#Gi@_z zbaXUX@yN0K0^A{I58@qWGXFKHNTi z1~D3qQG{se;}&27eR$EL#qpG(YBY(wyw@DTV32TdNTLi%DOA6LS&2zh5Xj>br7tSl zyU=7b>IQ=W{r$b@?)IRkrwgsEF8t=V@2aX_*|cq2{s-e02nGYmci(l7ux9PrQ6&lr z3JgRbL2y74B~XDvAOZ*xpzHdyiNF^Z@4cUj^z3&Y{H~>|s{^gAF0{3|;c~h7e}44Q zfErL&yporH@@6@15Fi2xXj&My88d~a)~%1H3{{bZR8UZ0utpgx9$CIZB7{KKbx_Jc zDFwI=&Iz3uymWzW*3%m3=G%)8lu&kM>Z7-gW8AruaSiL`D!@cUlh z_kh6IAC@dyVwGiyIGqiszH(LR>-V1N_YUNQgSaa%FaPuLxTG@vm{y{Ms&5$J- zgb--DhEOmFA`l3KfDi$M5D;N-0r&KF!DKRF_3AaFN)#3Sbl%9BzjF1O5(@JplZ78u z006vw>MM>$$7hw56{;i|U^ZJJ%Q7TUf-D(ekc|)}ap>wZCa#bA)&bT;7X zwJ#K%qdf1W?JrCP{-y$v*t2I(swj~k5Gp*bh4pCwr=lpS8 zX063yvEa{_FN1R(gZVfBg3QcJ2p~B1&M8D$P3Y+KC`Oav@V2}kzxY+aPe}j(cI@7f zZ4kxEwQJT{1jtJk$GlHWG^Q*73`$T@tX%)pdW$GY zsI6-N=Nh;^)C~gMnQp1cDzj#A|&{rlgX@Qg#`yZ4g_8K_u1c+l^VxJ=|PJ3p< zhQBA0WI&z60bSPy_elC+Co-l!+Bv^S6$=?HF%Nr~~Pbm+YGvAM?VNO-oG;?Fw2v3W6aG z)BRVFHeG<(B%`LTkvF&Xe`b|KX&X2GgVk)l4UNri=tGxSuIu37NJ~yaFzCmTqvhV9 z-)}E2E}c{-#@&ImRmJ-g<6~d^r%ex=T6+Q*@P`o$X$S;0I77*(aSh;ly%VaGNZzlB zVoD%jKYH}2*XQj+N>U;il|UH-Wg;kLa5TFh$woZAag&vbV#SuNTPD?saS3FsDk(~` z#pV8Jlik?b7eK!nLPw8+&Q>?Vst>*p#s4T-_&k&i^Nf4IH9fhsxU|ZzDE1>qk9Y_A zdyqWK21+F`CW0~%0%K@$wZULA;~zIYV-*Ejv1Lo{ge7h%kiM#@WR@-Vhq+JNP3?U_ zc$FZU+Xvuos)xlWLlBJ!1~mji8m3ug)H=HXBDevdwDdrg&+oS%EK#-%*=MAFI(*{xVAwI@{PcL6pxQ<2Si=P+zcDu``{DB(=+hC1C3 zTf7b5f9`oJlZ4;r$B@cToEj*bRI3p{=PL3n}V?#y(2SXJ%o=<4}h<9z>9 z4-=q51!qqFZ1=dv=NvmlflcW~ zTgJa9CC-e^NJ}Qxs0N;q&A2~aJ@txTi z9bm)i7>8<@hH^w|42;p;(3fwaVMT_wRv309xN*VrA%Wo|9BD1ah@l#6ME=MazIGyy zHzK)5@9b!`hGr`QJ^UCcFfzG*Gw3lyM#~?WB#sd^hC}~Fo^SqJOLw{{R30 literal 0 HcmV?d00001 diff --git a/src/www/static/img/agent-global.png b/src/www/static/img/agent-global.png new file mode 100644 index 0000000000000000000000000000000000000000..ae424d17e6a8a80552be5baf20cf5f3455106118 GIT binary patch literal 4014 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 literal 0 HcmV?d00001 diff --git a/src/www/static/img/airplane-emblem.png b/src/www/static/img/airplane-emblem.png new file mode 100644 index 0000000000000000000000000000000000000000..18ac2531c323c38126309f0a46b51ebe884512b7 GIT binary patch literal 764 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 literal 0 HcmV?d00001 diff --git a/src/www/static/img/applications-internet.png b/src/www/static/img/applications-internet.png new file mode 100644 index 0000000000000000000000000000000000000000..6ee500b28512cb2c5a412a41bbea337331191a51 GIT binary patch literal 924 zcmV;N17rM&P)`7alA?j>geG$ySKY0EPI@Xm6s!D zweCH5yy^y_oN~&gTmJ)&y!Yj|<0m&iIeoGxj!2?)gM__KJDecYBMjtkp~4zJmb3oi ziuujr&tH`O0}93Ii8qe#&rG~iOY8?dn5N0b_66ecKH`>7%&d^hEYSDNES@j8`qL%9 zvTRH`<B8SMBD4--omG>)W->Bg1A-V+$lc` z6fGgdnW5*M#OmgGs=LQ=!)}tcqGL#+DB_UiaJcU#*+iDDR+dhfLP;nLtl2R9rUBw? zGU4&0c8u!w5J9Nugc83K;`$v5hkijyg&k{<&n(b#yNPrM%?V)`#yJat-q@NVm;Q}( zauYjlvc7!=r8IU-5(Ek?iIf_pG~KBsoa!hoZ!0kG1PaL ziLtM7J&)B!AAheEcv2liN<|n(?6lvd*4XiYc>}oqcxl7SCSyEsCQ(WwrKH--)Al5; z*TMAyw)bqpP~mlKOd=}F^*!K*;gm})DbE`lIp{?Y5S{eZR?(Dnm% zS{70&n)~l_=YGZ0N>4fEl4}4s<Y9&UN*qdva+JMXS|isH$XstrjBy0000_E)I#0(_uSDbwZq&Ne7LR^6~1FH}Nn`oEFu9HAE zV@Z%-FoVOh8)+a;lDE4HLkFv@2av;A;1OBOz#wuMhzopr0AP(I AlmGw# literal 0 HcmV?d00001 diff --git a/src/www/static/img/bg-button-pressed.png b/src/www/static/img/bg-button-pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..b1fb90b18145a0ad4b2d7ebf9adc13b205928810 GIT binary patch literal 212 zcmeAS@N?(olHy`uVBq!ia0vp^;y^6M!3HF?%h*|g6kC$Fy9>jAhHDJ7@BXXb3l!lj z@Q5sCV95(ej@ z)Wnk16ovB4k_?5Aj8p}8Pv3y|DXMuug%+MJjv*GOlT#8B1djAhHDJ7@BXXb3l!lj z@Q5sCV9b zCYGe8D3oWGWGIAWq$;?3`UbpDQOyG?T;l2C7-Dhy?Szea%!WM9^1T25`=`CBJG}Jb z>xAD4OOmZw)g3lCM5!#a>1VHBcdY)(+M?BWwB|3p&8v`DeASVZua>bz%hq&#dYW?C97zNq$9H-l|`gZwi6# OW$<+Mb6Mw<&;$U$Eq6Zv literal 0 HcmV?d00001 diff --git a/src/www/static/img/bg-header.png b/src/www/static/img/bg-header.png new file mode 100644 index 0000000000000000000000000000000000000000..dcc54e6d91d638d2834a75b98e7125d78895e82e GIT binary patch literal 268 zcmeAS@N?(olHy`uVBq!ia0vp^l0dA(!3HEJ#0l2|DYhhUcNd2LAh=-f^2tCE&H|6f zVg?3{7Yqy{zKol090~ynvX^-Jy0Twm72`8D$x6u60}9EOxJHyX=jZ08=9Mrw7o{ea zq^2m8XO?6rgk+>DxO@5ryiZZh11c==ba4!cIQ;g8Aye(`Gf?u|FzuM9yolC_#>bh zEIa*~m6K1Y?Dpbzw?9c|Z_0gr_R?Nxsr;@luV*oeiYnLViZf&a?PlbP0l+XkK D7J6Mu literal 0 HcmV?d00001 diff --git a/src/www/static/img/bg-page.png b/src/www/static/img/bg-page.png new file mode 100644 index 0000000000000000000000000000000000000000..5f81a281cb6a037b4a90b453191c49e4f19f2c52 GIT binary patch literal 9911 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 literal 0 HcmV?d00001 diff --git a/src/www/static/img/bg-selected.png b/src/www/static/img/bg-selected.png new file mode 100644 index 0000000000000000000000000000000000000000..cbba7d5d9a015d3f3874b417b281d8b006dbb61b GIT binary patch literal 510 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 literal 0 HcmV?d00001 diff --git a/src/www/static/img/bg-side-title.png b/src/www/static/img/bg-side-title.png new file mode 100644 index 0000000000000000000000000000000000000000..5cafa90705cdeb50e5298d1bc32c9474e786568b GIT binary patch literal 277 zcmeAS@N?(olHy`uVBq!ia0vp^d_Zi&!3HG5esz}vDYhhUcNd2LAh=-f^2tCE&H|6f zVg?3&DiN!}mIwX*bZd|Q(!Qj>vP`+V7}Shn|8+^& RNCO?g;OXk;vd$@?2>_O3W19c~ literal 0 HcmV?d00001 diff --git a/src/www/static/img/bg-tab-selected.png b/src/www/static/img/bg-tab-selected.png new file mode 100644 index 0000000000000000000000000000000000000000..8953008b4a21e15c53311036fbd5182adabeec9e GIT binary patch literal 478 zcmV<40U`d0P)X1^@s6IQ*`u00006VoOIv0RI60 z0RN!9r;`8x010qNS#tmYDChtH6iorL*uqo*000McNliru)&&z32r7!QYT*C?03CEi zSad^gZEa<4bO1wgWnpw>WFU8GbZ8({Xk{QrNlj4iWF>9@00BZtL_t(I%cYb-cEcbH zME~%({|&qC1-fjc>B4}qjg$0+g$)QH&1i(azJJUIqwwLVkAxx+#^_)aCM=2;o&pRN zz&44%n?V>`sLj)xO`3w_wypqaMr_;IfDAr{wlyLHWI~NJnE<9tjATG59tVeQaxMib z=Ne4wbYof9{eHHFcT42aDFfUhhpz+%P#cRBAJ=C!c1hdbn(3mTF6F!M z08OR!wuG-l(R7|8xmZE zdPn6}XrlpB>=1V$;cZ)6dhYEz+StR*CpjA5L~c#`DCC7XMsm# zF#`j)FbFd;%$g$s6l5>)^mS#w#v&!8C9t`%hSa%gyXvD!IQiU3Op=^FZM?j vE#NTHj=Lc|L4}jySJ~WC9>&q{tdnZ^J&r0Uve_R2SgTe~DWM4fiL5u^T-3z`hnJBwOMdQR1ARo12G3BhWZQF(bRzVY|b-IN=ri`IUc7{`BtBj;|~Kvg7wlK87o2vi=5`E|8sb WF!M8`OoS)UDGZ*jelF{r5}E+lpJN#S literal 0 HcmV?d00001 diff --git a/src/www/static/img/bg.png b/src/www/static/img/bg.png new file mode 100755 index 0000000000000000000000000000000000000000..086e87735f6127ed47dca696ce22f967d5ad4203 GIT binary patch literal 684 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 literal 0 HcmV?d00001 diff --git a/src/www/static/img/bongo-favicon.gif b/src/www/static/img/bongo-favicon.gif new file mode 100644 index 0000000000000000000000000000000000000000..b73d3aab9ee36d3abb6dec70f345f70487e8083a GIT binary patch literal 547 zcmchU?N7{c0KmTyah}feV+axA!W|`wa6NG4B0XIF%qupVw}wr*bNQVYI}15+#CwaYSV4n+AeKfHgd+TyWCwL=b!M|XPP*-Ru@});fW3?d8k!6|DyJ=nHY2Sj z9$uvNEG<`Xoaga5Ix2W#q2n~27kFMn`yDzJT$gdy@Uot_mw0`bca?No>1v?+9`9TD z&`ghmk5BnjOW!lx&A1&5xEOqaryY+I&l`rEd~q?<%Wx0Fy?9?S(#J?Y-T_AY`R?Y& zXMCgl^z!Q){xQbCGVUWVM!-+-H$guWKK=v=1qcTTha!;(kr2^NPD0fGLBd3Aj7n6B zWD(`x6A@QcC7CVPt<^VVvvyaZC9g={IA2pD8CIpqDRUO=mkuu6EFWGpb6Ih!DqcRd zHg0z2p6M%X8FQy4rO()4J+Woefr9O^%X63LtR*|9#;nfXnoxG=cy;;iDPrDHo4LZU nbKMcyq)%S6bj7xlvN3V7YLLK#iNt~q8N?S_5nP=73Z04{N)SO1!Og8W2ucNCx(Noo z^#v@7wN#U0B~4S3v`w4b>kw!OCUB)^I-KEucpgq7ilWFNlypc92L#V}|Acn4Kd=90 zuN?zWU1DLDV3uyDm$hoVxWjTO1|X9oY_Ry0UvGO^ZJEXoR)ZLTb5k5m(5dq7KHWBr zGMztcEXDxjFL1JeEjGVWDWQ!`G-Ci#qfE_k1l>9-&pDnWwL|9A5J0=d{6k(nVC4h( zt6ZEWvBP>m_B_YZoVjw))8CqAY@9cb2p&_s&W&4yOLK#iNt~q8N?S_5nP=73Z04{N)SO1!Og8W2ucNCx(Noo z^#v@7wN#U0B~4S3v`w4b>kw!OCUB)^I-KEucpgq7ilWFNlypc92L#V}|Acn4Kd=90 zuN?zWU1DLDV3uyDm$hoVxWjTO1|X9oY_Ry0UvGO^ZJEXoR)ZLTb5k5m(5dq7KHWBr zGMztcEXDxjFL1JeEjGVWDWQ!`G-Ci#qfE_k1l>9-&pDnWwL|9A5J0=d{6k(nVC4h( zt6ZEWvBP>m_B_YZoVjw))8CqAY@9cb2p&_s&W&4yOqgQ o7XL|&jg1pI=c}CQYAj=B5WO$-Sk>yoRiHiwPgg&ebxsLQ0Ks-Wg#Z8m literal 0 HcmV?d00001 diff --git a/src/www/static/img/cal-selected-stipple.png b/src/www/static/img/cal-selected-stipple.png new file mode 100644 index 0000000000000000000000000000000000000000..5e3766db23942c39d2203ac7993c002074003476 GIT binary patch literal 200 zcmeAS@N?(olHy`uVBq!ia0vp^8X(Nb3?#Q1Xz2kdwg8_HR|aN=_}2Y1crzM-B8(+L ze!&b5&u*lFI7!~_E({&4vK~MVXMsm#F;M>t1_lvd#!WX4g#ZQFOFVsD*{`uOit!8f z=EPM1g=9-yBTAg}b8}PkN*J7rQWHy3QxwWGOEMHfGEx=XJ$(b-r>N!u6$*K}IEHXs kPmW+^;#sJ0z<}ungG6rf+!prFPk^!vp00i_>zopr03>rVbpQYW literal 0 HcmV?d00001 diff --git a/src/www/static/img/cal-selected.png b/src/www/static/img/cal-selected.png new file mode 100644 index 0000000000000000000000000000000000000000..a448dc8513c42cc22e2d0c3ec04c0b5cdac0d79d GIT binary patch literal 216 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU1|)m_?Z^dEY)RhkE)4%caKYZ?lYt_f1s;*b zKvgdo7({#-H{CcC0u*E~@$_|NzsAZa#%H5(ej@)Wnk1 z6ovB4k_?5Aj8p}8Pv3y|DXMuug*Kipjv*0;-=1USWl-Q?Fj&I!(I;7NRUq?r`-A6e jV>d<|D{8_H_Or2gTe~DWM4f<&8v2 literal 0 HcmV?d00001 diff --git a/src/www/static/img/cal-today-stipple.png b/src/www/static/img/cal-today-stipple.png new file mode 100644 index 0000000000000000000000000000000000000000..5897b0b20dd13c6a9842c04a1d1fe9be6ed51f8d GIT binary patch literal 200 zcmeAS@N?(olHy`uVBq!ia0vp^8X(Nb3?#Q1Xz2kdwg8_HR|aN=|G!?y+?~=76k#k0 z@(X5gcy=QV#7XjYcVXyYmGuB}I14-?i-G!IFffStGH$wYCXz^nX)CW5hc#~xw)x%B@E6*sfi`2DGKG8B^e4K8L0~Hp1uL^Q&jVS3WYpf978y+ kCr7X{@hnt0V8C>PK_WMKZVUV8CqP*SPgg&ebxsLQ03r@E`~Uy| literal 0 HcmV?d00001 diff --git a/src/www/static/img/cal-today.png b/src/www/static/img/cal-today.png new file mode 100644 index 0000000000000000000000000000000000000000..cd23f082a27cfcba76ddf670e97eb18f59487c7e GIT binary patch literal 215 zcmeAS@N?(olHy`uVBq!ia0vp^8X(NU1|)m_?Z^dEY)RhkE)4%caKYZ?lYt_f1s;*b zKvgdo7({#-H{CcC0u*E~@$_|NzsAZa#;0>eUKwPJY>8_`iF1B#Zfaf$gL6@8Vo7R> zLV0FMhC)b2s)D?d|Oy9YEC4)6>)2 z+uPUIw{YRYwQJXI+_-V`=FMBTZr!$R+xG3-ckI})bLY-oyLRo~y?f7|J$v`=-M4Sw z{{8z8A3l8K$dRK*j~+jM{PN|?w{G3KfB*iYM~@yqe*EOglc!IgK701;`Sa&5Uc7kq z>ecJluiw0R^Y-oAckkZ4fB*i&hYue=e*E<5)925hzkK=f?c2BS-@pI-`SaJWU%!9< z{{R0!(9A&ybU-lxiVFtzBMrPw%`L4>+8v!;oks0)a`F==P44A2*6PyHovo{4C+)rFLv65x9uvKBOarJs^$<5BnXYazorQrDfnw71+LkEXu(h-hH zsy@dwJtsXmF;UT_M?j~e=L8Fvh*rdo0!C*xUMY)?h5*JkPGR+cB_0bGE#(r^NpQ#r QU~D=&+alkLgN4Bw0E}-*Q~&?~ literal 0 HcmV?d00001 diff --git a/src/www/static/img/calendar-16.png b/src/www/static/img/calendar-16.png new file mode 100644 index 0000000000000000000000000000000000000000..018f518813cc8a4365a5b0adcfe10a3a3df0256c GIT binary patch literal 537 zcmV+!0_OdRP)rQxwhpC~pa=zr>~V8&aJ3>Kz9CZr@x@$+9 z6WS?lf9$!v6+;=Hwl;HX=XsvgtJ}e9ukR*BBLKg9f8?S4nDU$5)zXIa&-1jP=Tz{6 z>h;}Za^q0|K!6|Ns~CX*m*(eLy7fd5IK6P@EoU|b0Ij8ZP+C`_5kd3p;3oi!i*?M9 z$uGy@>B;Gow{`M;5cTdA1PJ)^9337LTZmErTqm{=DR zx7(%BXiRyNQj#L>=C(Kj(k2(B9S1SPE+371t1cRfEHf_nQkr= z3Yexzr_&*m$&9vJ_wHjoun@WEBk(C2jRH`qRAy4WeYZ~}5+0A>0et=TWoDD7n`<`! zc=7xtA`w4<9v%F@hOeLnjYf7dmjM~whadwqkzw&Z?a$CpCzxNAs bLG%4z-ZZJyf=;2700000NkvXXu0mjfO>XpU literal 0 HcmV?d00001 diff --git a/src/www/static/img/cbg.png b/src/www/static/img/cbg.png new file mode 100755 index 0000000000000000000000000000000000000000..4cfc20caa1e40941311c224e25c210dbdb3a06f2 GIT binary patch literal 411 zcmV;M0c8G(P)%9N7a7Ndk7(fAuH`| zRfWqWgcuvb@cnrS5CFozX$y?CAQ513EP!JJO`iw%0D}p4Q<4EP0A)uJTSTW}hsJ53 zU9C0TEQ>(~{$u>ENC^Jjnw$GoZnLYkwqV^!5}iI05Mcx=5ubL*jH=M zKvr=5L!c&*Zq=XhC3(Xf=utVxz_D( zyJ`@}tKfEY%R8Y6j5GCZ_P`g~5p_fzQAgAfbwvGV`vD_~obX$bPvZap002ovPDHLk FV1jcGr9%J! literal 0 HcmV?d00001 diff --git a/src/www/static/img/close.gif b/src/www/static/img/close.gif new file mode 100644 index 0000000000000000000000000000000000000000..773013bbe3e05afb8c44830003f8b590a6240415 GIT binary patch literal 109 zcmZ?wbhEHb6k!lyn8?g9efo3<2Zwj>-u?RZRq-baBLf37gAM}_faDpN?B?{ZJpGnG zS$NB>+3U9LY${-7xEgpca_)kFSu3Kp>d7ce$I8#@dgk`y)y5o6#kF5Kd)s!Mb$h1b NIJaAaJ&=LH8US}zDir_# literal 0 HcmV?d00001 diff --git a/src/www/static/img/closed-read.png b/src/www/static/img/closed-read.png new file mode 100644 index 0000000000000000000000000000000000000000..cf642b8cabfe4a1bdfdb57477ca6d58f167648c0 GIT binary patch literal 225 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4u_bxCyD&^*xWW*yIgeQ!D8gCb z5n0T@z%2~Ij105pNB{-dOFVsD*{?CO@{1d9X3|>=6p}4*jVN)>&&^HED`9XhN=+8$#@fA?!1y{l)FY2b`yn4YAT z({w|`_~9n5SVOf4u_bxCyD&^*xWW*yIgeQ!D8gCb z5n0T@z%2~Ij105pNB{-dOFVsD*{?CO@=LJvs87!Y3dxqZMwB?`=jNv7l`uFLr6!i7 zrYMwWmSiY|WTYy%d-?{vPf^VSDs=U9aSV~ToSe|m( MUHx3vIVCg!08fKF+5i9m literal 0 HcmV?d00001 diff --git a/src/www/static/img/contact-16.png b/src/www/static/img/contact-16.png new file mode 100644 index 0000000000000000000000000000000000000000..29cd84b86a28913f5568c6888aba3983755c9589 GIT binary patch literal 550 zcmV+>0@?kEP)w?!@2uv4s$`*$8Sy3J~P6?)h0pL2S8Oxl7u*piK2)w3<-jO z`B!c1Vc^s3IZC^KVxK%hys*TF8>iWM`5B()VcRyo?^CH%D3wa&ayfu3u z_d!Gy#+czEFWR#lZC<2YE~k~MDwbtsf%?Q*p3mPR2v^dJNh0jobqW!o-|sUR4ALqJ zg+dl+9=yPz1Ls-()k}fRP7%|zP*q&lMOD*27>1DrLY-|^XQ!Ey5{_{5e0Tn9vS|^0582|tP07*qoM6N<$g0VFFHvj+t literal 0 HcmV?d00001 diff --git a/src/www/static/img/contact-new.png b/src/www/static/img/contact-new.png new file mode 100644 index 0000000000000000000000000000000000000000..2640eaf58f4c12cdd788927c9ae79b5ae1d27730 GIT binary patch literal 2457 zcmV;K31;?*P)D;LA1IC1X_PaH=otCJNma+G;yCMS23-^x zrUJw6z_b>j?PkQRM~n)T@&Ov9Qz$DAg&}M+BAKjD1?hupfPlP*4`nX`e+O8LF|0O} z)rgqYDEy!_xJl44Fsm^0esTdMnN6z*v8zsiMjA2g7BCl~><+~0K-sOR1qK8Sa0E5B z0G+7AAczJGokQD6kwW~c5|G!hFzh-o7oe;r#A?F`wZdW>qsfA>0te)ri_uqw9=riX z2HX*FV+>Jsbu+MT4a8P#Cag4qBq7Dl^`x!8gl6S9JwbZ`M9S%Ds|N6Z?;s>IG! zPa@FW-F=^J+YjgS`6dy$;wRK|zZ;{z_7rv1L)6!urM6}q(~4qPjhHM#nTtS@xCKEw zBe;dL7&LtSCxf^4o(LRoZ~oU2qv*@$^Zjzeh7F%Oco9Ks zjn*31b*ELwaVQiDI1V_D!f_2;*Fbb0krW~c&}Tru3%U=HVMN9e$pSu_8gH=AoL%yD z3?o&_fa5qVfk5C>1zxn?_DohpKmsTo#2^Gj5Gqg-2Ym_w&_kTx#4*q@MB->!gk@Ke z%A~6xH|Z3bwdOxM`Z{m?^##U9W3(>4g*!HGCQ?;Db8zz^KnSQrOBDt+h#CVufHL|( zCxNmF0%M4rM(bKMHJGY~OlA}!jLhp4`u)>CeMNs(daCgdJN)d5+GR=g3{+ud<4n{G@?y}S~LrAbpy5$Wn?TVO5x5);GKW| ziQ|V}<+ha_1Z@y;$>b{;iVgGp&+p;EUmwNiFDnIf@ZdrE`}ksJr_$bS6Y-RXdg7R`d0e>Ax0Z@np5h`(Y zEn1qvSVlT~MqnPF68oO}2@4ymDYG@zm0=WcE0$ZuK`%Ss)}JTuuEmiyyr=Q384wX76}60yrwAA}XX9}cit$tm z#})D#D(hP()pSo!4mfHi@c{4SX> z1I3_j`s#P+JuyToXAp}gICFNGM8+kVaTp#+@`W#Nnv{Xdqb6E{XbGbwh=@7q`K$!I zz<1s~z;FJ%n~_4EqsKqt^hlO?N;n-)alE&mPaMrluk5@61ZG#STu?$qz{qHfhqry3 z6`yOtva-ZJ93(lI!Oc0iev4G4j}^FoXn{nKn8QA;cHtOoiFw89R(LU^^0{ITjSc^t4)P(9&j3!RdzPN#Be)S_9 zr%)QJl)50usVg{jHK$wQxmN#z40u6@-~0$%?KGZOHE#>dAns3x6u2n4{;f>K2cy!rMk zAQnEmhCsl_z{wBkXg`0_^!N9ZPN!!qq3MDO27@#;HC;Xe?Kj-S(9j8Lsv8+g^n*K@ zfao-q8ANG6$;1ftb!{`Id$z#6{nM4%B@?*sd)wIZ;N65HQJSNx5u8aUuz|wSih&bD z)YZ0e`bn1Pi%y7d))`_eD?;P`tKoWl8~pAe-K;cz)$x^)xZ`sR-> z2;x!*%oj5&0k4y#OK#?8KYjVKZo(^0V7{rmPT(>TnDIrnefxGqgeRYTlE)r<3=!e+ z#~&}*uC6Xbq-1yO*ijn$#1l`HY_Yv_=T3I++*$JJwYzrhD%q!=daC4S_wL>7-o3lj zzNidLWjtM0W;#K<&t}8U$L~xlKPv&P^^5^s%;a;G$6WaNNa>G!Pft%~^XAQgWHO1? zny#)cwrttL=;$b}>kEn{$SkXS56Lqh`%4GrY;dER>KEwt8zLLqLy{dNu< zI6!M_EAe=ov9U20ELgy*RjcNef%N_N-ycjQ5~WoXk<#h`IF5s9p8s>H?0(PSwJM<|eE=EvfmE zpHOeWMKZ;p%?XU*!Fvf5t@c=VLJL!ZcRjc{1G`+}4S1?=nWlYdo8Y=!m+Nv}t||E+ Xn~4)K36gfg00000NkvXXu0mjfXN;hn literal 0 HcmV?d00001 diff --git a/src/www/static/img/contact-unknown.png b/src/www/static/img/contact-unknown.png new file mode 100644 index 0000000000000000000000000000000000000000..6beef4675d59d04e4b03e72024ac0af1b6032b57 GIT binary patch literal 2243 zcmV;!2t4vS?|tjH{B9=Wbv>xGoOGE8^i{w7c9JEiQ9j|8W0P_7nrYL zH49^~EI=|cwro$ktE;QBva<3Sac4nAPF1_x_H;8TT}4V2m2vXi8|R)#VrE?Qx^?T8 z$INX2Av2c%k}S&>-+lL;zUVXQe+BgU=bzhB%D0)hPekN64m*w`>$=uuS+Y_}5K*p_ zdieF%UyuK2{bL+ascs$0*$%#(WR1?w9Uw-*z@|OWR zI5^mIUH7eP*RIJZiaX!BUDud0Ps8yQc3`5n37V0h)@&-k|crax)=-w$n)IF zvg{gw-(IzzcqN5I*N4NQ9F0a$N;P0S&x7y#Xti4KeIK6Z!F63Yjsx4aA%sAhrf9d@ z7z_piKy>rw&HkkrIyg88g%IBM_BK4vLs1mXqOR-0wrvm*>beFIL2C`ARKpekBuRo^ zuZJ{EWgN%50H&8=sO|ecwOTC{MFAp$Wm&K+3y$N!_kH-j-+;3$3qpvEST2`v90#7~ z0YK-nDeF0oBW&A-QmQFpB7zVCQpzSh>%W~ItEz(II3ObH)~#FiWdL;?$AOd*RaHT2 z4Q6hNxF`zbd5)^8pp=5vx|y>c29OX!E2Y{Ot|3~zrl}26A7xpBh#J6h&$_n@QjIA%sAl=TJ&PO8Ekyb(XZ&5JI5S>EPt#1aTbevMdiT z-OFy=xY6tNdVhc8jW{+cZ+6x`SEy+;c$pJj*;g%m>I6?Hoa|K9E~Rmf&i}TLTin( zEKT~f*7~>Gw{IW4R_oZ>+S;{kJB*@8?Ck8Isw$kEoPe3(I1W0U4m{7pn{U2}t5>h0 z)9FBK4aadX8jbMYd+#C3G6X?@AP7*FC6>!2hQlF-!y&reu8>l`y}!R7yt1fo+_+%_ zc&pRt$gQod#vcp#Aq}!J5i=-`VN3QFJ-7K%fGc+E$a9CkWykkpKtV{#bRN)c9WK!ot=%CPN!(K zT98trD2fKG@!7-S5Uo}VMAX{f-`_qL(Ek2@D1?Z5y`Jdz`9aL3?qobn@IWX3CYikQZ5J)NIwNF3&)H)N8lyWZ!0@>+w zkfy1T`VF07=B6c>^h8mF(P)J2?d_)6g%GH!Y6Fb9pUq~lEDPOk7q)E+rPS_uDGM#j zg6q0X&oWKa+#A1RVm6!M;lqb0%MwLV;MZS&eZdQz4y`q&(<#C*gk@Pm2r)Pl5HtJ6 znwmG&S~tdQ-JGq*{rmS(6a|W+z=H=5HWpq70-$+rnx@UEg_*+_0DbbwCyo#Tj^i|q z(fm;e@dAkTc>MS=ilV@BxoiaREZ4XWN^6ZQ%MgZtV)^jH55tXZ&&&=0R!VI!V;rG* z;Ay`M07pkhST2{CPNy5`JzaaIAukpS7;m+53~bwWKtxMs)&c;2{PD+JYc0yMgk}9% z^vmTEQp%UIV}L(@{`>`Y&YEl7rD49VYiO;JBuQC>mh%EFV;04j=N?s;BoKA*#N-R5X>I$dX7bEZp@#1uI{di1C{^Gg5(fIQEudcEFAYfZLo!*QHOWnZ>{`E5F#HrjLE z9K(?3d8X!ck|c+}|Ni?NfM#Z8F*6ep-@A7&+u7N9%FMfES&FMyuc9alEEbE#+@E(D zMP6U{^k`WY+U+*l?KXb-WCiBG} zbV0600YHXfXbB+%Gt=t7wAvIKL;+wTVj|)^&sABL3V>dPxv!bIer=O)>S_&;30fP^ zt8`uqeJT_HbspQV^^I|rN^uGZodb&3M!gze3gS}Vsp-Gl>MTIK4(K8^_aCH+we8-y REQkOA002ovPDHLkV1jJNGQ$7> literal 0 HcmV?d00001 diff --git a/src/www/static/img/continued-left.png b/src/www/static/img/continued-left.png new file mode 100644 index 0000000000000000000000000000000000000000..0b1ab3b32ff99a3482dac4e6d306388b8ff4bc29 GIT binary patch literal 179 zcmeAS@N?(olHy`uVBq!ia0vp^{6Ngj!3HE3xL=O}Qfx`y?k)`fL2$v|<&%LToCO|{ z#S9D@FBlj^d>J?0I1~aDWH0gbb!ETCDk#Fn|J5dXF;Gah#5JPCIX^cyHLrxhxhOTU zBsE2$JhLQ2AtWPJ!QIn0;C+f}9#EmUr;B5V#`)v~307s^6eq)tHEhcmi?tYPG?*%P T#VybPDq-++^>bP0l+XkKq$n^h literal 0 HcmV?d00001 diff --git a/src/www/static/img/continued-right.png b/src/www/static/img/continued-right.png new file mode 100644 index 0000000000000000000000000000000000000000..d50233f5f7b60f47a56773dc50680ccb007fafb9 GIT binary patch literal 178 zcmeAS@N?(olHy`uVBq!ia0vp^{6Ngj!3HE3xL=O}Qfx`y?k)`fL2$v|<&%LToCO|{ z#S9D@FBlj^d>J?0I1~aDWH0gbb!ETCDk#FnD{W-e4-}Fuag8W(&d<$F%`0JWE=o-- zNlj5G&n(GM2+2rQaQE~Lc%P!02UIBL>EaloaXvXgf>l{C#mO+RW*K9#8beV4Q>A!T Rg%MB%gQu&X%Q~loCIACvE)@U( literal 0 HcmV?d00001 diff --git a/src/www/static/img/desktop-big.png b/src/www/static/img/desktop-big.png new file mode 100644 index 0000000000000000000000000000000000000000..d9dc4925fe976c0196e76e433b9f69e0c0a4e4ce GIT binary patch literal 6407 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 literal 0 HcmV?d00001 diff --git a/src/www/static/img/dialog-error-32.png b/src/www/static/img/dialog-error-32.png new file mode 100644 index 0000000000000000000000000000000000000000..e71bba3bcf381fd6abc263d7ff32b25f0055548b GIT binary patch literal 1550 zcmV+p2J!icP)~1wgiRfDmYqM8C-~DlZ}D~bSJoS z<3>>emWmG;5tM=K6eJ39V44}B+Zj6PO!ahkb$3l@0|0Ub468o*isY80MkGPXaTEL)#gJt1jtzcY9cPMAaE6US#7(NEf#}pp zzx3NtMe=#F7cUZ97B{b7_q(muFBIOWs+v;)Y9e2>GMQ^nE-d7|T#n$@EieW|dH};9 z2E2_7pO|=}Ci2w-0Kh8~v$Jl-aflm@qe91tpY-nV zvoS;~D}3bXr;8T6c>utIS0`s@OVQfe@!|usduRmq+>VFn-oH=j!UeGHr)nY-09H*T zV?;hTGBZQGxp`tE3K&&1^*Lgx&w=~F=OjUWpRo%UBH&B?2+U+WFR}pB?H*g%41gW+ zISh0R1VGekF;XrUfKLKAz;q^;OH2^-oJ5ec(x;b}c-*r1?6V}T);@R;;N6aloMHv}Ja zI~}m?y&=#ZI`3Y(w3q2*Fyoz+{RZaA;C1ht;}AZ0fWqB9fG`Z*#P^AsO~i2!+Xma- zzqc$@bsyrC;W;9nE?3iTBh4lq-!}^X18}OUYrhfk{jDv}L=k3Z$oavtdIt~J?LoQj zzV#soS4e+XOcG2Gpg};==|CLg&djj$-g|A}pZy2`{IRvQ^{vs-Q9JSdqX5RB#z1fE zPVW5)-z1kK214H#;O~%TLWSD))>d0aM^Vc+7+=pR20L9za#~ zA8{Biwwg`jl*^AKewq*2EK(}5QLlFue$bD~-gd0;osISN4kIIw%YA6QRzO9_Oi!~} zuZNl>Z&g+A9s#JT{>Q}e@7C(|2hP+KIL>4DdN1^q3Vs;scB9eKft~OGKpKT_x7+R8 zjqUBwnVJIEee_4 z^%}>)tyJiU=<56L2k)wW{|%^%NMTU(z^Pbzd-TK%%zPJqaxRm3a&mMu=NLoWYN2OX zhFBI(sf1N5vc0>TY`0pSKfn+F3hw|7U{h5$jsqA4CV;X9<=5eB&%o7!=Q(5fyz7Wy z!Vrz3US3F#>sS`zIM}%y>bm%yj&AMlMjsfn_)GZJaxd*RfGuEERog=WQiu`Y954=y zPeSE2xcYJUf^FMYK9g~>j$_%Dg_Wjgqe`44>~_1|B#P_})jPk3-@XI4fgNBw1-LgT zJ`^B@uz(U!21bFB1;sDJXP$v8<1ptzISZv2!VYZj!p0KZxDNkb>xudF{+u^}hN=!p zv1j->O<^*C2jo)ArsmR{n-LHWSR3f5>Zz0F|0FwySxajMQUCw|07*qoM6N<$g6;_V ABLDyZ literal 0 HcmV?d00001 diff --git a/src/www/static/img/dialog-information-32.png b/src/www/static/img/dialog-information-32.png new file mode 100644 index 0000000000000000000000000000000000000000..c3c07e15de54e222cbee38e5151b1d36a3f13760 GIT binary patch literal 1921 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 O00003z=Ialx9i#G& z3;rD~Kq#pA77qj=<8w1G>Tx$w+fc*RD_w}90YS(k6b`ew5n*O_j-H3T0G&a__wo=> z-{d)0XtxjbbUz^SJwo5J=S+Q=WMyduv)Ra{u1?Ol-8hOIAi#~Aw*V@Fif>T^P+aO< zdiytE#JrCssJ>w4)Ps#1lz&qX}LOc(GY5M7Lx3 zCj!hbECAdCIPf6kQ~cLgLZRQ?#G$OL98ol2GUgN6h~W1J7#$r&)QW8H>=0CZt%n^1 z72iO8lLxEKN?(6J$z+mLDh&vvQz=wcMO9U7R+)<(ZS+2Qn(F~TdwUxl9T#}*^-|wZ z$7C?bgnyh9)zwS|0#vzb5OreKX60o*B%MTMMFo!1QkvbTDJ*i}C~;Ebs>NcpU@nl6 zBzH#~A}Zhir~wSW@m5ejW~>S|mR z$TH#ORo;&&jEx5XTGrRX%URc$ag~>W+kguw`6GV?J_7fHif;_g@O6LFo N002ovPDHLkV1grWPe1?w literal 0 HcmV?d00001 diff --git a/src/www/static/img/document-properties.png b/src/www/static/img/document-properties.png new file mode 100644 index 0000000000000000000000000000000000000000..0f27c8b9d6df97cced5202437824f2f0e917ca6c GIT binary patch literal 716 zcmV;-0yF)IP)WFU8GbZ8({Xk{QrNlj4iWF>9@00J;cL_t(I%Y~E8OH^SL zho5`V!MUR|S0W<&kcqOLOklIgMbPv=q!Y9Xa{7P>{HVoUjgGT#`Ufh5ASDn)gEp<2 zD=99gaF<@^%P|~hc=J6i#>!#L4!n!^!0&m_`JML|G~!w+o&3cAz1RG5~Y+>t5s1-Nh+O`R5}?utThi`N~IFTVv%B@NTE=mR4(CWvn(vm<8667c{~cR zbQsbE5D`QKji5ykErc(GDVNIxtN`AY$L;tXfTiD>wkCiU1dRxA0281y5aiw5TdLJ6 zySuwh?Kc6m9EcHIH;d~o0}#0qA!v4U>EdOAT_F}0=Z^+7S^z}jy35>8j1aPI*4NjW znR!E7TZD6G&+|_c8vu<)h{xkR&%8ji=1flyLqoS%`LaT9Z!gj)vlXEC_sQq;h!*^Q zKf}YrOr@r&*Y*g9!%Yvh07Qh(*(|fOv$(DcfMr<>4h~`%W^>u5+3JB(iq+LsM(*9k zFx$ywGW7TN6OCR)L}M5Rw%y6*=H}5;@cDf7_4NTJ{th3H9wh$!3lX7GE|Ew)WO8zf z4uAX69t;c&@bcAbzJ1LR=nB#mvI*KYLAx7(o$WGDC&sDmDICW+-7-eYvWUfEPgOo=$%UjANHcYb}*ZMb_5V#Bm&{*K3mZHYD$Dh}K#h y#}VTI@DaFv+#c(FO}+LL$8jj;JpfLeJjE{-@kJJzGexlg000002zo&DG8|STB|dV3#1iv6s?^CJ;*jdL(|yY%E}`i#`n=7s>QiEAKOw=wArj$iE_RLw;oh%rCa3=Y8({nYXKcX#cZo6oKlr=+ zf2qOq|Cpx0TQ3c*zYFbc{;xOJ{vT*y`9ICh^F*qQtRL6_H3@O)%QL$EU!BzX|Aad4 z|3n#?|JtfX{}rXw{|oR4|5p{2{4c^U`v16!=zm9R_y5MGcK?06LjLX7QTS*t#H!06 zEGVX_sbTV8Ps{AT2%pq{1sUD{F3!RK4Gir6>uMPP7v&TCuOKe_e}atge@k=c|H~H6 z_}|)C{J+-HvCyq4_)sNtOSS;wt~e#nk^>*arSjjF11HogNQ#iT-~H5t;ug zaw`7~Gz`wGDC+*7)LHg_uMT57Bs%HO}&FlI< zqrdvUyQAHIeSP!)jt*`=6y#)WR8@3;2L}ZFU%qJee>F9o0I&hd>bm>X)s6qFXjuGH z(=^JIk~93@(OU6;+xi**9qirDOGzrOmXncj0jZUjQzUeOmDyNw_)LN7sSNG^#~@O0nbbT00030{{sL=l&XS9-}vbO O0000$S{ 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 N002ovPDHLkV1gPT*unq+ literal 0 HcmV?d00001 diff --git a/src/www/static/img/emblem-readonly.gif b/src/www/static/img/emblem-readonly.gif new file mode 100644 index 0000000000000000000000000000000000000000..c42067a516d884959025f38a92dd3c257c3b6453 GIT binary patch literal 576 zcmZ?wbhEHb6krfwIHu0f(bc+Y^@>A>4;(#q5c&iJpFe;80t8>d;2Q{h|Mu<2j~_pQ;MdPz zzkdDx^&1HO{Q2|u@85s_{{8>|A86Xp1&Tje7`Yhg8FUzc02C(->~kBK8Jb&KTbURf zy1ILMnjJVNPMSQ?u745><19v&Nz*zfF|zUturcy*an7H>&BrJvE-uL^#LsKf&n+Oz zD6goj#3&;!xTBL_WcDdW1qt(hVVTpj)#Xh4B^58v(pJ&zmsY*asH<_mQ&ye*GKYcI za|ao9J!5klds_<=1I-UoYC1_b4s29D&MT?^%l6g54Ga6)O_l0|Q&v1$z}U#ZU=0Ai CO_i?z literal 0 HcmV?d00001 diff --git a/src/www/static/img/emblem-readonly.png b/src/www/static/img/emblem-readonly.png new file mode 100644 index 0000000000000000000000000000000000000000..9319cc247b010eb7b05bbefcd98d421e73990376 GIT binary patch literal 615 zcmV-t0+{`YP)u%tlS%x61UY5_a^L#X-F_JE*hn! zNGeUhur>SxhJZ_5x$$2xY=kIMp!4-Q@4n+=Iz#!onPgsO&iTE2Pp-n1s@1BGilxD~ zBJwz~$v6NEcmrpuQ21`U=1rxt{PU_Ouf}?PVWZUykB$y0BFsueczF1m8?9#8Sg$YK zXs*_)Gp)_0Z>^<>u$y#gOw8Csj7>~y6DBrcYqRNB>(!ae=Vgbn!QsaIvlk-(aB%R8 zyL(>(-w#FsU}$JKn46m$esKSxdS0F%+1dHr0Pq9=3K_t3&Fs|FG!mO&Z-3YC_)&SO zvaFUW%PKO#)BUe|zUdm=oxTUtHM5y72WYKJN&zAkVH6dWQ$MnQORrvk3xjY>MAEiB zuXGKdlmZDj=K%1=oE%-Q%`>Pq-j%h|+n$%k;hclDQbGZz?`ze|04E>`tvxi>YHgvs z#OjBYb{t0}$tVhWa{7}0K4kpK^VXd9;kV@0`9Kl5a*K1zB_J5f+@(hqR`fXm{viQcy!OhF;v39Yqn;%Ic@&LmuZGcN%yG9X-U ztS}6rwe~##i=Vc)J{F4u*S06i;`%}nVnqR1{13uEu=s|Qp56ce002ovPDHLkV1heN B8l3A4;4vhDMBKPEn(x^Ha2x@ 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 R?>VpD4_W=$#7M{Se*v5)Ajkj! literal 0 HcmV?d00001 diff --git a/src/www/static/img/form_buttons.png b/src/www/static/img/form_buttons.png new file mode 100644 index 0000000000000000000000000000000000000000..21c6f57bd1a76d50b2ccbb2a3142642f0b78f337 GIT binary patch literal 3371 zcmd5;dpy+X8lPw>$0bVF69!+H4L+kBo)?Y4;q)* z%E}@uhH@LJX(^QR;lo`M-bO-#pLrKF|Bi`+mRg_f2!g zIH)M;C?OCC7007TP9YHTEbx17lL8pCyD=FEgz|#pk%O)=Bb+g(U_$ivmie)$S#^_Z z^erWY`;}FXnegR%HYr@vwu1_Y&KVU*80jb z(!3&k?D_F*lli`KY@>PFzpj3twlKH4Tk$|)cAbI5YBj#l0crHm$6>&Dc)*>jt7iXf z3v7jP>1*&o6Xn4uQV_OJ2yDFWYsvvjJGXztf%`ozUEZN6X611B`;Otg&$az-n1SKN z*(vT!-MN+4o{3RIX3?R9H!WyWRc(yoA6gubpbFBUCm5ps-4Her+K`1?BBGHAv9r{w z>T0;mPYbi5n0shcq%$$&N2d%*)JYy}LEk+v;1n@KPn+fP9!2HzEr2uAF_7DbCc7n? z)z;PmJL$x!xE!B@MJq1B7casl^X~CYRCrg!8C6wP8-V@OuOkiSG($tlmYJCuV54`I zHg(F2I~J>IYvHEL;z6WozetWAunn^N8IyRd*t@*E9GGkM0w*7Pc;a+{-S!`#49UHszK~L9u%Q;x# z&nLVIaK=7Ij<*i0r(pSD|4byX4YC7p;k&;QwjIQ;vRs^S=7H{pz@4PLE2oDlz|aYD zVpcPrk;dzIi4sS6HHMk|Ij{>%*lRG2#r+)@A@t+cHzknKC|B)0&)V1`n7hHTqTphP z6I{sq_tht@a~p0&ZeLXkPptouJm*0js*i8#Ow%vg6@Q1CrLPug^J)@*DBelw>G*m7dCt*iUS0S;a z>{9q4(TBWsLJOzQv|C<{@#Teu5;IgIQ8XJ#J0H)sx0cci4th%_V@qcmn%pJAQd^%L zmi=+>S0<~F)b4KBMP_?7`G9)j@_+;1%buyhLd7k}o7viK8@2C+JdE`!Ex2I0gvZ;R zmRt@F4TU}#T7%we`2_|24cHLQ%UgNvD9Y5-PFPY($jTv!wyrMZ2jtgr`GiXgYE{k6 za7{=M8(O6eGAJnMW4y^N&dzO!@d51mtN|vaqXS-xYxUd;(~BqTKwL_2g)5VN!{5)IBiby_$l3RGw@!zync<#K~8Oi=>{}VS9Z65DbCD)(5O@@(!^B> zw1!YV5y-h4t;wR`?`mjhR5dp4_`rc1+g}8SgxHr2Tt8O)>;Kc2I1!&iKZ=S6LSjMa zdNn)@Ywo*Xu46=V($i%5m6erMd28ANXC}4cOr1}|S7Tlu0Xa+(* z$p&bbg7Bv0wBLp05Rf3lp&bf^tZ@6lwF^W`otx(^2p}w@mRL9zyn8pq@ftVt^71}Y zmZ3i?Nyqg;xb0ggRYNoHbL%anGAotBAd;sD4Wb4afu!4V32%t#MBc(==g9^uxL0qS z%P7Uib50SFbW56b7S=HfTXCW$9hTG1yhE}Mw;LRt5X*_3eQV%rp{3=fRBs*ahFPLfEPFxK-ii7%bVYSprCrp+(6PFrqa^B z+@jG%O8DU>_Z~u*|Lp86+{Qd}otwG0aUngYdA4}F@M4|+qR)s5?kusgy!_dxZ!;$! z)tprYc;m?MSJF94Ajsew9g!~T>pK-394xIaMb2Q~x$?@&b)}(DC?03dKwI_-veaT; zA0R~oTS_NIVHbf@8MjVOOGbXir%EAQS&6Ul+ymyz*u*$*erGs%{h2j$xw`7Fzu@{Z zvIqG4-!VhMdNSLG&^%}o3)3^8C}1mx{N9cz1A9%F1_C+qQttja5ZGU`Q=rCUv)Pa0 zmoN1{z<^nZ1O0*F?7rfdgm=OQxrAB~-gVxnuk5!peh&z?BxzO@j5iAS{8kSoo(o;g zWUNsg9-f^YluBifdXg_S! zla=G=sw6E^OGE^tcsxlCxJ#xx}X7EVZnbx$d_F0}BV+PiB)qU!Z0NPgg&ebxsLQ E0DYZ0@&Et; literal 0 HcmV?d00001 diff --git a/src/www/static/img/go-previous.png b/src/www/static/img/go-previous.png new file mode 100644 index 0000000000000000000000000000000000000000..c7367ed32d2d6e5f39a5a41ba37b53388637fd62 GIT binary patch literal 219 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!73?$#)eFPFv3GfMV1=8YnsS=J^Qf?)({>_S! zla=G=sw6E^OmdKI;Vst E058Hp-2eap literal 0 HcmV?d00001 diff --git a/src/www/static/img/grad-cal-title.png b/src/www/static/img/grad-cal-title.png new file mode 100644 index 0000000000000000000000000000000000000000..25ebb71b8140ca9a8e03405e5db51f7c02ba342f GIT binary patch literal 957 zcmeAS@N?(olHy`uVBq!ia0vp^DGUsZUpUx+tka)676U1^ByV>YhW`xT817c_-2DL* z;VkfoEM{Qf?gU{*^OL970tMMiJbhi+ud#@TiEv0w+iVXMk}YwKC~?lu%}vcKVQ?-= zO)N=GQ7F$W$xsN%NL6t6^bL5QqMFCRz^v%$;uunK>+Nkvp(XPjy|M+1YPfC3Yfi$enkJ=q)rjBtDDz-Cl}`VnXs9oP;% zBQaoqzP{G+L!y}%Ibttvzt-WAyg?C?!hoUw(O?QlK~eh+0mhx5Cr+K&rz5R2eV4Mg z*%LK|fAT?`OSYyt=X5m0ya>N$(iSDn#L{ujq0R8hF6Vp45}dzqFZs^5y|>7X3vh7hKk g-|v1G@DzW^;JDEFcL858D5EiWy85}Sb4q9e04>Jfwg3PC literal 0 HcmV?d00001 diff --git a/src/www/static/img/grad-cal.png b/src/www/static/img/grad-cal.png new file mode 100644 index 0000000000000000000000000000000000000000..c85976c470cfbdf53e55f7095cd0021795d0a9ca GIT binary patch literal 423 zcmV;Y0a*TtP)f}6`vr3WDMF6z6b`96dxDlQQMYTQshgSIg_hBcOR$zUNl!^8 zslAVVf9}67&3Z2nMF<$<6s2iNC+hhWrplQ zguEzP@%P>ScVFZj(8<6*sKD$#)aU%eX0;7_UsIkkE3AN*2<} zn|V@?q8v?3FlvmfbFmd%{#7u7l4X-MuO(wNSb4x0F#3`;vuvxzwDR-|qhru)z4(** zk;qk-gD?t=-jEj5O-pL1*H|oPA}|sLds!#mT%29P9a&aho-F_7r zzQ)CQmNNjjgszot0N@A#SX=pQ+!;DQP>NrU+t#g-f)rBWYVeGH5$-tcUb|5aFvXpG zSa7Cz25#~)L&HLN&0Kd^Cy%e2RypDh6)Fvi-rq`f1nm-}cz)TVpmglW)yE|=hdc*W zp^MhF>h>uw&s=rjcl)MNebQF}uU6z*V$FF$F3Q@`vMn8A%jgak6fA9`J_6vbyv_w+ zPXS313k3J0vmK{^@V=7=NdEBGvw_c3|Nbd}>_!M5gj#^+z6w4I>Wk6%NIu6GRGCQ& z@wuQHcex&*s$+xy`wz+6fZJ?vi+|&q9A*qiB)%^|jstB!(xBP2odxqQETXp`0eF@Cbs4-XpUC1%TF zrSZ&2`!j`7iEeKBkABgAl$zTzK;iQ2(#4ug3gc|O`LAj=2=6Ao$HwXpAIB%Bymbz; zE3A2wGg4iKQ?OswzeZHZeM_~F%RT#53*O)YJMHHR$YrClj#UZBSaq-0ofpDgUg z`PSsw8qK)8caCj4Sw33wcwuvFQ^ds9fQg1qT;pK_35yhFui=tfUb)|R?a-aX7LvY- zn^~6zsNq({uM0qOY6oV%UQ^W=1Hd1(a9?EVUJhw`)`^KFW17_=zGZ%$`+y50sRV*0 zBWmpCmCK#|FD*O;F7xs$w8s|lEyW-y{y$s;X@$xaxenkaTHap8gYKA|^A!N++8IjN z1Kp7t!^xE#y5eX}zvBPxP1>47NsjW~!-KUvbuis+uVOdH>H7o*!b~||M z^+p>0T9NAw4oEqMv{}f;J3ObUL#Di~!hXf;cg}j0wPWJa?wGj0gt~ zOx@Ulm}yu(yZJ#mp+{@x4_6TFZ_{Z7AP$cH8D>NW%vjrWfN?~6`uhFoG(K5i9A7=^`S&(#f^-K)FVjfTAn3n-4+5PbgSB; z%IKcMZ|K`#`EIXSHn|L_KOv$fh;Leg1PbxSuvLsyVYsL<3Nya`os46Rmrk;6R|rxq z#VM0!{r`vE9=ePOB}s-#yU-7ZWHRXJRh7|nVi<2ZsokB`9r;$@Xhq}(;maqZZgIgG zP}A2;;dN4?^%17mp1WPPnTwE+ddFy@cea#&CwyK(wgdhbfY6X|>E%^PhyMkI C)~?h5 literal 0 HcmV?d00001 diff --git a/src/www/static/img/grad-tab.png b/src/www/static/img/grad-tab.png new file mode 100644 index 0000000000000000000000000000000000000000..3b9da1893e9761225689990f24a23f778935b031 GIT binary patch literal 369 zcmeAS@N?(olHy`uVBq!ia0y~yV2S{;r8$^@q<(l>0+3=$@^*J&_|Nc-;cgYr-5)>^ z&H|6fVg?3oVGw3ym^DWND9B#o>FdgVjYXJWTw;+8_`iF1B#Zfaf$gL6@8 zVo7R>LV0FMhC)b2s)DNp%c5*|)9?L{85mAVFHCP?a1noaaiz#|50KRLzN~lg z#~B%#%$>{^17#Mfu{a1aH7Kz(Sp3srcGzNH(RuFF^R-v)fgWV=boFyt=akR{07#2@ AMF0Q* literal 0 HcmV?d00001 diff --git a/src/www/static/img/grad-textarea.png b/src/www/static/img/grad-textarea.png new file mode 100644 index 0000000000000000000000000000000000000000..6b73103598e925dac061deebaa74704c255b15b3 GIT binary patch literal 218 zcmeAS@N?(olHy`uVBq!ia0y~yVAKJ!nK;;hFdgVjYXKxjK_5K?{c7!Y>8_`iF1B#Zfaf$gL6@8 zVo7R>LV0FMhC)b2s)D3p9zr)78&q Iol`;+0G;bYbN~PV literal 0 HcmV?d00001 diff --git a/src/www/static/img/grad-toolbar.png b/src/www/static/img/grad-toolbar.png new file mode 100644 index 0000000000000000000000000000000000000000..34e24b1a6c53fd6b59b3b6013ff4501c08f45441 GIT binary patch literal 394 zcmeAS@N?(olHy`uVBq!ia0y~yV2S{;H944oWZKgM$AA=DlDE4H!+(Zv40o$|?*0IZ za29w(7Bet#3xhBt!>lxWU2bfP@w_u6JYHAjs4J)S=}pp~$je`r<%_76FE+ zN_P&BAdtyrDaE?Lre&Q!LxM63gCYnutP`k$$Y(jf&{AYLtJovJ6tGv&3aHYe<#C2c x!!vP001Zm1^@s63(rw&00006VoOIv0RI60 z0RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru)&mq1ARCeCAMF4D0W3*G zK~zY`)mG6CgD?n$rT_oFU!7I}Y#j159F}MD!^yxN^#ByxLsuQq7smn5}OKINbu*6xgo?K=ne{22ovMCc8px zG1zD^kcYDCPU8eb&#U4i`|%raomJ({d?b*)bTyUY5x9_?M4%yWo)*Zntb)QhOuHs0 zRRoy?{YA5@g>x5mTkMJ+NMr~plzK0d$YEs##61=7(b^2GRg4^nJbW^?3QN33!Rxtn zogATS6i#_eyLzArHxkwY+#0g*pj^6T^Rm@;DWu&Cj&(|3p^r= z85p>QL70(Y)*K0-AbW|YuPggC76}niNrn8AQ9vP6PZ!4!iOb1p2|$pTcrr05>C&I? z@9(>^$uw|!)&Kka{Cpx?(!u}#|9>|Q;F{rn*F)5tiDBPfwHXiHT3!GRVDNPHb6Mw< G&;$U(gER^N literal 0 HcmV?d00001 diff --git a/src/www/static/img/grippy-v.gif b/src/www/static/img/grippy-v.gif new file mode 100644 index 0000000000000000000000000000000000000000..7797ff6a563991fc57bb97393a4b916c3f155e57 GIT binary patch literal 91 zcmZ?wbhEHblwjasIK;wGUS8hV*tl`y#!sI<>&kwOg-1|bAh%e34Nxf7)5S5w;&gJt0-*#oS3_>@(=2Lg zYOCx2|9g9Ug7U>j-|PSViFDVEH~91a|9|_#3)s)Nu&n#Jzy3d8(qm<2H6iBdn+~|~ x-%*sf(ZV!+$^kWz9ZOiv79Nn5YP9{qz##S6?`ELq^K_s^44$rjF6*2UngF0lM9%;K literal 0 HcmV?d00001 diff --git a/src/www/static/img/help-browser.png b/src/www/static/img/help-browser.png new file mode 100644 index 0000000000000000000000000000000000000000..3cb912e385863c1a6bf323d5f72140d274347b8a GIT binary patch literal 2343 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 U6qG6lj%-n{eS8@~H5nlQJIU*2-2eap literal 0 HcmV?d00001 diff --git a/src/www/static/img/icon-simple-calendars.png b/src/www/static/img/icon-simple-calendars.png new file mode 100644 index 0000000000000000000000000000000000000000..39fe0056178f51d31486c5911025a3ee88711bf0 GIT binary patch literal 304 zcmeAS@N?(olHy`uVBq!ia0vp^f21sU*ljAR+R}gmS_bE?e%nV46!)9_0mSK!wDisKgN3> z*tq!o!;q3wmlRj?KX{=qMQ<19!UtEj2CFS?_Os&L=WyN5MR5Lsvj=xADVWGI$;7tc zVO{!<;`4j&Crq=CPgu0~-p0lyEN5gFB))jR;dQ`u!A&nLJPfZiS2e2qzt_yyb2sp@ z%>&{46}8f6NxyHWK6C`@H2=S>c8>jALHFzj|F_M2!u#C9QSguW yn!?u57MoB9z3c~zcUj!FvyYA{-N)wjpFwDb`rEn^vuvP87(8A5T-G@yGywpcOMH(2 literal 0 HcmV?d00001 diff --git a/src/www/static/img/icon-simple-contacts.gif b/src/www/static/img/icon-simple-contacts.gif new file mode 100644 index 0000000000000000000000000000000000000000..1e1ddb5215fbedd798ffad7156740ff225b98fd6 GIT binary patch literal 352 zcmZ?wbhEHb6ky-{ouhR}G{$yd~Vz6h>VE_V|w<4-x^&WOZl&000McNliru)(8X>B`{~}bY=hm0X#`W zK~yNujngq}R8bHG;4j$?LLe9re}woC>FHR zr?iwoRW}cD(!swhIbBE102{a|IqgpiRwM5&xL`ZPUopTt3HJ1SO|E^aAz*!@&ZBgBgRWZgy}R@HViL(=VK16x;}B zasGb?TX^3##2gF8J?kTzGFY&+JM9bsIJ@ltt Z{RK9KX01Xs6kh-U002ovPDHLkV1jMlw1EHs literal 0 HcmV?d00001 diff --git a/src/www/static/img/icon-simple-todos.gif b/src/www/static/img/icon-simple-todos.gif new file mode 100644 index 0000000000000000000000000000000000000000..dbfe3e94ee27138b3de13d0abaefd4c768927635 GIT binary patch literal 257 zcmZ?wbhEHb6l36HXyIoFPoJ1sF|VM0ap$xxvsWCPv*OUaRfp%VJ~DsJk%eoIE?ReN z>BbXlwx3vPAXFTC+b|3npl#WqEMb$lA#cik*eVC=^MbH_>+Z^i$Q=v zhXDvcj$vR;RZ!{b37EIclI@_8Q-i~aFE;!E4Lk?A9hlt;G*VQU`WqLR|Jplm=kdgs z9~jwhtl`LfU^>q)V~g~UruDZB{dt6yxwF6NceiR*aF$7PF?=Z~%E(kotf^|LYEA0W P=dA6YP+O_5$Y2csZO3}w literal 0 HcmV?d00001 diff --git a/src/www/static/img/icon-simple-todos.png b/src/www/static/img/icon-simple-todos.png new file mode 100644 index 0000000000000000000000000000000000000000..ca40bcb436da5d3ac0a0fdb8aec5d2d1b0aee5fc GIT binary patch literal 367 zcmeAS@N?(olHy`uVBq!ia0vp^VnEEt!3HF?4d0vvQfx`y?k)`XK`<%)-F~15XMsm# zF#`j?6%Y$Co=Pdr01C2~c>21sU*ljAHaC$dI~2s&oCqlbBOqtxs9jph%eUW9))5!pTO=mRy?4E7Ap z5`pIE(5=syeatU3I@B}t8Trj9Yh1=qoST+?HYDqOM10QL8~f)Z)nu=!XLFxF zr34_b`N7g9mM+nR$Ft%RtE-=|bcrIrVd;{P>S5^;#D=paU>XwBkhGbCi+yQqE6BY^ z^Q>2ERBKh96qb4a;WK79j1Xd*fG(S32#-?5WuKvNx;sKLrs1yF>FY@nx7+>}@G*6t zXc*efAXm?|tX~<7IAUj?&?zu`?EnOJHylgNGR4T233;C~* zzti$Am{cC>$?2>P?x(b?VZ@5oO- W$;QjbY*^#~0000_3qO4@YnzQ>Hquc|NHCz`|SVx@c;br|NQd*{r3O<`TqX>|Ni{{|NZ~}|IEzH z000000000000000A^8LW000{RA^sIZa%Ew3Wn>_CX>@2HRA^-&M@dak04x9i001BZ z7ytkT{$QawWNDsgs;+G7zHlthbZy^wuJ3&B|G=PdNGuwU$RtgqY&xINsB}uLTCdow zc8g?kzu>TVOfH+x=ycetZoA*`xO`5pzuES@e$Vgw|A1e3frN#Ihlq)aZi9=CkC2g) zlawcom6(~Do1C3im!6@bqok##pr)y+tE{bgsIIZHv$VBNu(r9nyS%-YBfi1H!^Ei~ z5yi>M%gl%c7Zc6W)6~^z2@Dty*4*9R-%Sb&8Ws@X=jiF`F#-n-8W|N767==<_xSnx z`~3a>{{RCD97wRB!Gj1BDqP60p~Hs|BTAe|v7*I`7&HFcFu=e91{XAl97(dI$&)C% zQ>t9avZc$HFk{M`NwcQSn>cgo+{v@2&!0ep3LQ$csL`V?HvkAg06~GLP@_tnO0}xh zt5~yY-O9DA*RNp1iXBU~tl6_@)2dy|wyoQ@aO29IOP8xs0C@B2-OIPH-@kwZ3m#0k zu;Igq6DwZKxUu8MkRwZ;Ou4e<%a}83-psi(;th8_iylq7wCU5RQ>$Lhy0z=qur-4Y k9lN&e+qiS<-p#wW@87X!&jwDsxbfr2lPh1&oOlQTJ5EzWhX4Qo literal 0 HcmV?d00001 diff --git a/src/www/static/img/info-bl.png b/src/www/static/img/info-bl.png new file mode 100644 index 0000000000000000000000000000000000000000..0aa033865e5b330816316b2275798f7620b6d176 GIT binary patch literal 500 zcmeAS@N?(olHy`uVBq!ia0y~yU{(OKB{i(P=vF< zBeIx*fm;}a85w5HkpK#^mw5WRvR`8r7d6zc`txoJP)N4KHKN2hKQ}iuuY|$5C^fMp zHASI3vm`?yBqLS9-P1SVeTr%x0|R4(r;B4q#jUq@Y`tUyC5}J*KXa#ioL-%%lLL#Q zD~s=2r6%nS1&Zqwe8m!`y2UnF?`Vx!;yF=t$t5>#$ClZW-Ps3>4%vpzTW)o@B>(&4 zGuBU^RsT9X*>2{2RtAOz%Z_mX-NMMkz`!AZjp5J$6a`74GX94*Y+1(0z!1VeVc+}R zOL%;l85mYbE1b>v`t9Yz@Jl|EMSxysc*W`=X?Fel{`LQsZ1LPJm#qjiMmM!=X215` zFV7E7m%n`SSKK7Mm-8mq?Xv(Hprh?DDW%NNLau!A=C|jUKR!EmaoMJg^MmfR0No(U zq~clpPj`Nq{hHd`-R}%OA72~60klJl)#KzN`#p$f literal 0 HcmV?d00001 diff --git a/src/www/static/img/info-br.gif b/src/www/static/img/info-br.gif new file mode 100644 index 0000000000000000000000000000000000000000..90baead45bdc01f0e8becf1bb1a4b59fb6699221 GIT binary patch literal 259 zcmZ?wbhEHblwgox_`(1Jl^G1HCbAq_%y(h|!^tIFCzkL$x!C&k-KB5uuYUV*{r~Tm z|9`*w|NHg7-|zqb`SAbG$Nzu7{r~sl-@m{A{{zwgGiT0#jloVR{$ycfU=U+a{3q&M zl$uzQnxasiS(2d;l98(5?&%xApaaqeats4&`U9oD6i*=st{H1OSLN#LTPbsS_1Q1MN|h->a~)T#Q`~B~YEw(1M~Vku^ZjHWz3GaAU5f;Mgx>p5QPX50 m!eY;zc>euwUgq3N`cdx@v7EBiH8aZwY|gYAc%fkLt+t`Q~9`MJ5Nc_j?aMX8A;sVNHO znI#ztAsML(?w-B@?^9IsfC^uFx;Tb-9Dh6YY!;KDNbCM1Q*LtTtBgroKgwPD{x|;~tK!FsS${P2 zUSt=qTzdwnczseGi`RB{hV71RFRt$?sNcl6rl{TfX|ks5ksop#3Y-y*`Rms9Fe%-L b{_TFB$vKMe8(Up6(9;Z_u6{1-oD!M_C zX>@2HRA^-&M@dak04x9i001BZ7ytkQ(S(Aj%k2-NoV41Dv);V>4~F7Mmgb43>dLm5 z;l}bz*Y=I)`p)Gm#G>(tOe&YmW>M&jN~hGS^@`0-q1vwZ3l59Nl-XQOsoK0jGV08dc5rX3~kvQOi(P=vF< zBeIx*fm;}a85w5HkpK#^mw5WRvR`8r7gf+a;Fd8FC?s3r8d2h$pPQSSSHj?2l$uzQ znxasiS(2d;l98(5?&%xwK1DSTsBns>i(^Q|t+&?=1sN0sjvQbPK55%3ubX2o^zMNC*Ip am@oIEj7_Le_)jKKCxfS}pUXO@geCy5WCrRi8>dhCYGe8D3oWGWGIAW zq$;?3`UWuQfRuwYGBBA;>F;FZXEL`Wb`_PJ*OAFsV YoR#E}U|zicdx@v7EBiH8aZw{B{e*+3fI_k*t`Q~9`MJ5Nc_j?aMX8A;sVNHO znI#ztAsML(?w-B@?^9IsfC{ZVT^vI^j=w$YD99kdz;JMJ%S9KNr!9i64DxbQ9+XEX v9KQMZgGYX6(8&W#Y8#a1OG-e%otdaOrZ9NA`njxgN@xNAA&ouM literal 0 HcmV?d00001 diff --git a/src/www/static/img/info-pointer-down.gif b/src/www/static/img/info-pointer-down.gif new file mode 100644 index 0000000000000000000000000000000000000000..fc3176cd8c4a623d29c4079a081514a6071d0bf2 GIT binary patch literal 422 zcmZ?wbhEHblw^=(SgOZxd?CZB#SCYbvYcGOdTJ%dnbmxkHW^;sYI1Fx-SzGEH+DE& z+v|OMkN2IuKDUp=-#d}@;7rM*v!##E*FL`3`t)-9vnw6Xu1$D;W5%nyi(fxj^XB2& zcaPV-d%ErW>(k%fUi$v-^7jw7e|&lR^Xs$!->&@me&z3vtN*{>`Tyhbub=P#|9<`V z&*y)Czx@CA_22(LXUGDIKUo;L80;Bz7=QreCkD2|2j+ih&sgelVu{eotWu7{i3cuA z>8@WrZJB_qS=92^o33su*u9@2Lp5xYe1_t|i$+@~K5>na5ZJgTH-)X*p0y;gQMb8; zQKq!ByM;+3C#k8$iIpcINk^A`h7+?yzjq|(!WzX;Z%tjUl}_xsI;vjkx_le$S@?Ce z8&r0P25e*&aPw5wl}tY-w11n5t~}H38j&q!npzCkYs6C2c3iukDW0-!!Lv-k6kXwS g_gUCh%RYURDXq)+Gl-w(53eGF!@T`WjtmUe0N>T(ng9R* literal 0 HcmV?d00001 diff --git a/src/www/static/img/info-pointer-down.png b/src/www/static/img/info-pointer-down.png new file mode 100644 index 0000000000000000000000000000000000000000..a4efe5d65217d3afb22e20df4448beda5130ff8a GIT binary patch literal 876 zcmV-y1C#uTP)P000>X1^@s6#OZ}&00009a7bBm000XU z000XU0RWnu7ytkO2XskIMF-Xe7#1EI6LE}V0009ENkl`ZodcISCK%+%V_yiAP$!yJa;`ON?KJpUQNziQy5c+K-( zIQHaA=R8_7(3~by>4!lT3Fn3w{bt97=l1-y1wh{RkYoG9$}dq^83xr2h-`?+0niA? z5lWVB-o6d-`)C?nf4@BZPJnPgb!9{x0HUyjvGuXLA^@oPlN;B*w)KUJ;M}_=q!CL>jB}e8WBJ*4O@+Q&>L1^q=<09;;e`O_`CVh<{HN@{jhft*$z(k3Q8;O!~s@`B(a727OtP0 zn!Vg_$=wgKZJAD*DUuVJ5WkPe1fzV65QIy7|vUQTiHez+_@YuI| zLW=@-69{#eP7*{HHYzOLnUG|5>PKq9?|=Mx{>=#CCLhB)=O;zlx}I(q=%+Fxejg@w z7Is{}^6Ay_!F>_39rS~D#8m?z?`;-Jde3A=(qlbubangm#g8T?hTmCM9R&+K3 z+azDyjBU0aUlWo-b4CZ-u8n>;HTmlYEo29{y|;$dgyf68#8G(Pz?o$I%t#8&7&q|C zzr8mzHF_*YxCx{@Fd#3A^3a-*6q+<{=$D2M%uZkUI7X=CFvvo(`JIo&7`FZ= zB!x=G4Y!RCzEvp=?T@TApzZ;nYs1T)q+=)RRRf>XbN(~$yOFccR_j5@T5D}EJ7|AC zQ7n`FC%z1H1|eeifQa<|N1yL6bZy+p;EB#!oUcByZpc5_X|~Ho;ftQ$y?K9JH0KmVP*&}&K=cf+6=`N)t+N#r3msWY``E=>Ho}~2i%BeT&nAYSey-;_( z(V8_sDI;{|uE_fBH#=;&k-|1N#R#+K{l4W-=yRVJpZA}1{s|5@KMF(v6$FFCn84^7c_`ir2#iT!ETV;}X$c$LFnpvfL8sWGQ|!~VC+a(r zM?Z2$J2|7B+_A1BW8JCapVAD<41+S$(3>&Qn>p2=JJp{zjTcVi_h*RW*})Q{w$wBv zFpmi5bfP)E*fL&c8Lyu=yjb`lvCgzwXWuUw+m_8;wneSYI<&r|cdSf0ZAPcf1wB^iU4q_a=>6mK+M#y^ z`qrS&27Pu&I!V$CNjD@tBuT=54Sf)j@c)T(QTzZ9fL-7p8&5zG`AAS!F06PgdLpiT zT2(EIVb|1#2PV`Dk%)l(e6&G;Z1Yb(jTYuML^4w{(OWs$Sv4`+H>abwFJ|>9sv<)Y zFBM&FQ}&8uqN&Hwo99)^N@Q#NnW8JhDlrnmxm$Q)Vp4up*QSl{l|H<)>3~7-CdmjEjnc=Y^V3RUm$YeatZ&7Vu1NF>E2}bgEdtK zl60Mt!x=Vfx9rZ-)xF}kH|=0ks)i#ZLcA8)d#xk;S>IQ26t^rQZPn6OoqY1)hqr)t ol+Iwq7F6Ul#tSL2r%FF>;!(?Jaj^x?;yV;NH4sS+Wl+GOzeU}Qh5!Hn literal 0 HcmV?d00001 diff --git a/src/www/static/img/info-pointer-left.png b/src/www/static/img/info-pointer-left.png new file mode 100644 index 0000000000000000000000000000000000000000..3feaeabb40b34bc515ae2ea4a1a80322660c4257 GIT binary patch literal 957 zcmV;u148_XP)P000>X1^@s6#OZ}&00009a7bBm000XU z000XU0RWnu7ytkO2XskIMF-Xe7#1HB1hF*`000AANkl4U9HP*h5=iKQag5-=_Fp#?!jDk54DeQ+NZ59`5(n&L!<(v7w(}&5_pfOIi1Bd%|e*b&6dlk!T3Q%gf0s|VL1G-wK z;KmHswIu=+R9k=p1OTV0p75!c=~YXhzM$MHsNcGo@cvy|@5-RFFTA3|T&#K%2m%qH zvgaOJKYPPGa`w10`RkdkL}KiF5fNFadL6KV5};yxCu`0ebzcAG9XmPrRZlWCab843 z#LtRk{}i>r+?xe|?P)fxUlV$BMeKpn*o}uBD_9GD3UXuM#Q{Oh6Hv&#)qWj;7Y_xW zD6{RIEuANVk=pGb8E}6CKk=`t7h7Dj0C?dL>o&KAk49E)scOFUSirC&ki7)COMuK8 zpjd!iUWRkxd3#?)!02z>^r}(0>W>ejtA z^&QWdNCq+&K-{7`#+VDRe>brmTSEiUwgW4pH#}+P^M4Va~5Pd4LC*`PbmTASPga^on($w6vwY zZo^>{1hT__51Fz+3*=8_fo6d+^C6U)@n(^)o8+cF3)9=Q^zCQ4nNDlEmlu}rsnC6r;1?+I6X@%;rLop0MdhZIqRn_=VI1=q*YV14WqXQBj z{d6)labEY_E0%7B{!pfI&hsaK5Zqr-j03rAuE+r%pd~Xu*6Oay|RCsJ;s1&ze00000NkvXXu0mjfe-OLe literal 0 HcmV?d00001 diff --git a/src/www/static/img/info-pointer-right.gif b/src/www/static/img/info-pointer-right.gif new file mode 100644 index 0000000000000000000000000000000000000000..0e1e3cfd759f6d125f7671c43908e8fe605a93cb GIT binary patch literal 676 zcmb7>YfDpM0D#}kc|F^lnv!XhgQ6%jD{GdR7GzRRO^b>Ig{&~_^eOrf&C9$jrR-)R z6?mCzJ7&1%C2$Rr?aF(_q2DMu^ika7S!FxbH+6@XMQpo|S=9FhQ98CNR{B?&)D5ulZY zlbry{ITR5<5rO0fKoLP&ITsSaT6q}R1t5Vzf(<({ZD;uKmJy@f@p@%~UU^vmDbb)x z8tX|O>lKXkCXe@>8t+e?_?&JW$TAKHjq0pPwQzc{V0y4{hAf#O@6N(|bHn%Nbq`D< zBJ-$tL0_|AkXR-fEECU{j7`fw+N`r(*13->^WCfFKHDFi%{sENVsNZYIc@V!o5{6d zMs_{28<1lhIgH3LiJae&^QXsYMXnj-nnNxVa+{Icg4|2U^9y;b$h(5PtH}G;=d~m6 z8uG0ppAGr!h<4Jn7twA+duWRx$<`APNSBE!D#caRHMNq* zbrR05`nq`kh^GxTc;->RZ95vP3QF+vNz9#Z--)twuHM9j$FQjP*{bYIxA2Te{;T>P zRnGO&%ZZ`NFB(-|F@7c>MqNv3u9aRm5G&Igdh$!|2tuc-r!^P*PjF4*1x@a73btHf z)#Mc6sUhnd)D1k%9{JnCea&NP000>X1^@s6#OZ}&00009a7bBm000XU z000XU0RWnu7ytkO2XskIMF-Xe7#1HoMgy&U000AjNklN`)xmL%|2NTN)G;Eu|$$9~2b(pby0tg+5qC`UzUAG`5YU3iZKC zYd??@p-rkHGzNIE*eNXeTeO=ny3aMfvN})o&&Bp)URhZq%)I0<^OVW`}ZC5Pkeuc3{VKt z3q~K1d!~qpWF~)+8<*NMy(c2+?>-If-S#Z4z9n3|xNENl*6=9z2zP{?xZj0J;AIl?MUPEC@Fv=-lkxng4UJGj;S# zCh^%%)m=fx!mQ|lI|Sh- zn%X`H=|*&UhkSU^GVR2^PLlJ1jD=sUBDlXpcqJ>=zZ=r@$o(&=-RDiyN$fey&0uq{ zGDwL8UJ}d}mOr%1&CyRXIe8;NAi$vb6OTyTCq%b3>5GbRR|MWj8rASG+2OAlo|JL!uEO4CuwKxqa_3n`^^!qFAFRlBY#5?iez zHLHo#tU}YRf1Tn~>Kg{Sw@iM1F8kQWUoftI{Ic$b(zTQ?xk2Bz2hkWoF&a2SM=$~x zmQsi-!Vl_NY`x?QtwqmXriW`hYss2oZR6%xZDT@3fekVPrX`;weh8BN8VEk{F1MHL?Uo)ow O0000I2!GF-R-{o`lJbC%2*qkCJ#>DIZ^9FCE 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 I>zopr0I_SOFaQ7m literal 0 HcmV?d00001 diff --git a/src/www/static/img/lightbulb.png b/src/www/static/img/lightbulb.png new file mode 100644 index 0000000000000000000000000000000000000000..0c54f5253be0b2493d00ec45a67742672276cfa2 GIT binary patch literal 1233 zcmV;?1TOoDP)X1^@s6IQ*`u00006VoOIv0RI60 z0RN!9r;`8x010qNS#tmY3ljhU3ljkVnw%H_000McNliru)e8z0B?5|CCK&(#1YSu* zK~y-)eUx2nR8blS%EWP{)VCjf`0jo+(*Xe0_gl8S zx>RVXPi|>la?hgHI~g)Vk&?(~2v(X^;e4+;@$1QxzUyr6?0o0^EWp#xzuan>ji*=M zwMI*p$un=NHV{@y_nJGkZ z2rzsm@&%Ktv2dhdM@qIe7EadCD(An03bCftXMJ1RYx^hmd`}4V7P**CYd}U3Y1du zuwA>7H6#n7#PdVnvaBosuph*|Y2jjUwCaXY;uEpQ%#J3ws!1`$K&fH}XDE(KnUq#Z zr^M8}Tv{tnOcVfsU1zFCiz<<>N$<}>xs<5bUU)0F@I5MV z>-NH&+;HSbcN_=7n*hMOcfUP0UMfCT=sPW%HG83yL{X<*>Oi{9jDKBEbJv{Eg_e$7Zr;rP|@Xn5$-NJ^JA z%gii0cd}O33wAg8hl&lP#`#bNs#q1A9ziVnRjb4#pdGT^l z+K$sTbC_h^`1FJA6ED2<`e#d9mv-o=tf)>FxX?cd)11VVeId*wE0dn?o-v(kzg=@0 z0btXndsjElzq`Bb_I3F?+EyTEWx*^^ES7Ne==Xl1@Ym*jAMg9(nh^ki=Z!ac_KCd4 z5@*kqFh1@>(-d^+;_^^WYO*{!hB-0|Kt!2*-oX0x_ha6TIS8TSN@)Zqe)%1K5CBMO za{)XXpu$SUh3i$3Bnn9!!7vQuawbx`27p?x3rG_3m>I>9J~&PpzVE}fYw&{zQJg?2 zF*p}6tBBjS5uZA>{0$<0Mky=+lIc6tK$0Xo4j=w!r&22VAApExx*po}Dmywl-q&@x zF$kisySlnw0}uiT07P|FN`-ZRHtnndNC7Ybm{Q7I6h%6K8USa82Gcwc6hbmm72G|20~i#4vT*V6)}KJVPN9v>0ilk?B1ctfsvs=omdsjger)} csUYFN!&}_}kvJ8sWK8%gGr1r#gn_{t0CsgR&j0`b literal 0 HcmV?d00001 diff --git a/src/www/static/img/link-underline-view.gif b/src/www/static/img/link-underline-view.gif new file mode 100644 index 0000000000000000000000000000000000000000..8b847c19d2d73e468186b8c330fbb9db54dbdda2 GIT binary patch literal 78 zcmZ?wbhEHb6k%XyXkcV0oq6N`|Nn~rM4gLL6H8K46v{J8G895GQWe}ieFGR2f3h%g fF)%XdFaQBa3j-5RPyb4WWA_eC4vY+CV6X-N%KsMZ literal 0 HcmV?d00001 diff --git a/src/www/static/img/list-add.png b/src/www/static/img/list-add.png new file mode 100644 index 0000000000000000000000000000000000000000..c541e6efc84d3ff61e3a49076183d0d5dd6c6f3f GIT binary patch literal 400 zcmV;B0dM|^P)L zQ#($>Kop#@T z%cLqvB(v#cW*r_3NHSe47Dle|vSb9sk?XofZlwp~@z{#v*z!Ejf+d!e)NVV?0=<4H z0PuFUMM|xfBa$ox?ALVp((8wx4<8?%_|3t5HZPo$k{}6=Rf?x4Ljb__%~hUdS#u9onO5laL%FV7IFst;pmq}1vHzn$nx=mj uAo(_#UK03-kR*w5>x{eb8#ImbT6hNsK5INnouuRd0000ERFvuI;uvDl`*xBcSE~Vob9|S($WQmc z^3w0s9D@C!FAN`DS!kiL!(xV}w;ZR-w#r2xH2yL&tk{%Uw^j8n*Uhx(a#4nc!Wk1< za#aMAZCX`7#y=}=UNc4Lr?g^>z79j|{d=({l~#H)AovX|9<&$= zMb94fD=7GKuwNp(7tzKS0ui!19##{VB(_Q)_%j1D^LzgD%)^NN{ZE!72o12Yxj~-i z|8#KY$<8E52*JwLyHgKbPTwGHIm!JG@1{HY9gm;CpV%;Wy+J&0h@yxf2xvB&a1dx( z%(vU9d`KVY1E$@cU8BE6Z)>aM$Q;(ux!^U{+L2zoEJi8hc$0v&^|b<^HJ$Cw7 zS)@Lhz!tJh&T7=t?7ozeQ{opl@&AtYs~Lg-&}EpQuk zNV?OXKUb3YT9y8CMxJEYG$Z_)5yu&GrciWjNLUe=g&6L2%f%i$76@qoi4bZ+g)Id_ zqn!HehO`zSr*^!B4!=`td}HuqKqYYSeFxuDxURx=CDsmSgR*SMjoVKT-#@arlsKx7 Z?I(-VhCfBedebn_@wlDi_=j-B|cZ;vz z?tIxk*L&7=>&4|lP`M9VU0dx;r;1BONopM&7|i7^%F)=;he9%y+}zsyE2dNw#+atn zvb~u^JP~h?XENDJr6S8wRaHYQS$;pWFgq4WO^Tvu+xFwhBmg!(A5Gt;lsdL?|HiQk zqjyiAzxoGTm>s*BpT75K+jTtvynbsYBDL^5Z&}XtlXcs%1yLNo^DsYgt%3a<()T<; zN}udV%s-otMF~)u@{IC~6WEF4?B$DBOr~Pn8Q=2`=H>&ta(Te>MFJjR9RU$h;&61h z!*yLsiDPYr#O+!Iu%Gf$pJja3wX$ibhM}5wk)Zvu}k8Lmz$LY{Q1ZAldML-e(iv%PF zQn6hVCE8ZO2d&mr0UsTLwqOh-W5-I{0f7N5+DHg*(7~7J@w3R0?>TV`o61nxHj z$j1Zv>YHFt^p;`Ule`D0UUU|0TMK)FQm8B`Qzj@bD#||~g9Hcjcgyl|M){|*0>;jQ zd`1ekJ;R2x4?xhWR7}X*Sus2{8h0Q-68Of`_P%HE2$yoo^D zu?osX=E(l?!Sl5R<`2IfiGYWQAAa1+PktYNSUo=lH>mVkuNARg{N%rwR#n|z#<~vU z-M7&Wrqv;~a|)+@8qzy@Ls+35mDjZXtIzUO;^ZZ*Ub;EHG0EO%P%6^VF1nV+ta~Nj zS|;72kvPKt^Pxn*e2Pi_B0%wKu+@gNBc;-#VwGG?^z>T~_8Sn1Q{v=t1m-p97K3(S znr;fS(v>u3-r~75Hfux2v2p1h={qSQq=4vj?*I_EFCV+ajNG(@S zyagooM(IU^>*ud;{Q5veGePCK1X62H*&x&@-2|WIDx|0jGRRWNddYZ;L z?lKjAmZK{pE#FQ@`@ls~8hWC;EEsT*6wc90E0MV{(Fgc=C-uZDCFnQ&O zW@>JO2MCK7(?}v1VDnW(rKhrS@%Zwv4Z-Rs>tnhB%9dNmdIw$G_>wc^{DqW3@UG4) z=BQu|@fI370pcvxW$GebB-K1kPrG( zT{OHfv20@HN_RNIM@CCm!s?jxYY0+A8CS;I&UYa`^^@8d#x7?o?Hs8zZ$^CRrxGcB z^-xN2joT^rFlXN>kh6WZP7rM}D#;sNVX>Zn5|&^)F{*fK5Q1#C>~xM-uDZ|s%;wjI zT&>o`RXsO^k6OkKuhNT@G+*nh6EWQ+!`Bs4h6ezCOB<3?>6M0l-;AG-BtMy)bv-^z z(TiU0(rvuw5dqHzDK62#N^U&7W<1?;QmWI9)yT}quI!LluWew-{N5`r&tpeh3ped} RcLw}?AS_8Nyw1-(`adO;H2VMm literal 0 HcmV?d00001 diff --git a/src/www/static/img/login-bg-middle.png b/src/www/static/img/login-bg-middle.png new file mode 100644 index 0000000000000000000000000000000000000000..c4b939ca9153288b16c381d004610805c37563bd GIT binary patch literal 442 zcmV;r0Y(0aP)DPbGC#%vv>DN zS(+`slfo$10+u?d?Yy*s1U!5GYXb>*R}<`T6*%Gf(+)=gzuV!SbQL(^H=kuJ;C)T- zq^rOQ&phd{XW7G*&KhkG6OIC&2D;#AQ-MdD9d5!^=!84kP{5m3;3(i9ngDy2Nt^}z z+PczNqsEs-fG_lb kcG7?SJ&FRpN}hC(-XTX#CKG@~RsaA107*qoM6N<$g3H{s5dZ)H literal 0 HcmV?d00001 diff --git a/src/www/static/img/login-bg-top.png b/src/www/static/img/login-bg-top.png new file mode 100644 index 0000000000000000000000000000000000000000..2cf97bddc3d2b2716a4b0e610c3ed8f5466d97f6 GIT binary patch literal 921 zcmeAS@N?(olHy`uVBq!ia0vp^Gl6&~2OE&I{xG)(NU4uk z@Q4J`+`=Ht$S`Y;1W=H@#M9T6{Tc_epoUI__^!=BA=whwh!W@g+}zZ>5(ej@)Wnk1 z6ovB4k_?5Aj8p}8Pv3y|DXMu43`~DLT^vIyZoR$h?t(pSIVH9S3H-xD~wP z@gzByvN=txiH>4_H0@+x4IOltbd~t(LM%{P@>Bnnz&EV-AnoyY2U1 zPjS(GcWbVh^6eCH@%bi_Hz)AkmtlIib5G>9*y9dcPU`9JHSj$DG`!uxBE`sU)=HK8 zDdFdv+SC?TO|C6uI}$&AzAV$j33uaW`y5iVE@nIO{L^CR1Ft8&@-_P>VsfeY=Rw5; zJwc-vXP&01W@H$(@K_b^lx5}hZJXejuIKL$()+jSLfQ=PRpvJf9tm)?`yA_Ne{(*6 zvS*dveQTCOiq+3~KI~djGubwerMKWj-(1FLlRo)=OVPWSG9`c0_Hzv-C)hNVWjF2b zR}(Jo+{v=uds*J5x04r4oOjxskFnVEt6Ff%5$A7vt^%z9SzcN41Zc^dqxGGKW0Ld^ z0`biH$+B`*EGCjyXXpG>%4l&mo8kSa+$g=lTJr1r6uYk=H3wf$I_Z3E+AW@9&!0PO z3OHJvzh&upw);#>)GPeho;P`GxFguO*OOK{Z~L^8|Cx|t^SzjNyQc{5pZs$!P}kp` zcgi_tc%M&YWAj~?;QRm*-R1oUJ}++zh~#YDq=x_ zB2;elOx|d^(HT`Iw!1rX?7liqR!q4t`}Dz>MJEM}UYuJhpX}MbCU=V@(Ak`GC!Cb{ zI3c&@Rr5}u$FHkizJK!1TOnV!pK6nh_UO$#IQPu^lqJr~P8du-lb}}%RJG(^%qzn~ mEC$d#_z{?67(8A5T-G@yGyworAD$Bc literal 0 HcmV?d00001 diff --git a/src/www/static/img/login-logo.gif b/src/www/static/img/login-logo.gif new file mode 100644 index 0000000000000000000000000000000000000000..3917b04830754fc634d98fd0006d44003f48e1a2 GIT binary patch literal 3434 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 u{ES|~WdJfcE-*1JGB7YVMR?c5 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 Ye{Fm-DuHw+!vFvP07*qoM6N<$g471#djJ3c literal 0 HcmV?d00001 diff --git a/src/www/static/img/login-pane.png b/src/www/static/img/login-pane.png new file mode 100644 index 0000000000000000000000000000000000000000..28b28d7890868bb2c5baae91bc76a4b3cc30872f GIT binary patch literal 49978 zcmXteb3kR^7jHA!wr$%@Zt`TirpdN#+itS$y4g*3&1BtlbMJnC@4Y|H#Xe`Rv-jd> zueG97lw^<*2oNA3Adux`C4WFbd`SdfOmHyZk<0dfL*OqMQ+XLlh+hzy5EFu4GIQV& zct=?s7YGPM%>S-05ZSqS;6YedIYlYhRcI^-K8WFSH&^fwv8$A}tAvA{orS$CgoLw& ziK~SoM!I1KRcx zeO;eB+Uf0vwn$Z##D*^YaHU zE|?&=0y6$L$H`fye%0oKgz!8>=oBV2Gb;;lfA8vIhKY%ZM@T49zbetc%j!`3A5Am# z*sQD~7g1s1xQU60(Lqx1{r+9Zo1qT%Tv5cB%DFOk#B^Gn_MW7 zaeBHjx%U>1O-Pv8Oq`wyoSsF9C*luwJ73vPRa2sODEuFN{8)(yl>NQI5ba)XZtq^R zvG(}&Kn2Aa2nZhAH5H2Z|Lw>EFX>{ppA>>k#NXEHjlHpGEyR@Ac7y4Nu5{ilgF7Ru;Z78=mn}x#B74I=~jy z|AF!|mRMOB6pS+9jFaAY5ql{l+I$hjp|rB*J*xXQDQFZnH8(Q{683p4tYWx-T}EUl z8%d&+X?(tCu1v=Mxc->R0D{{6d=LgaotmcplUMrR%$Izv0?rS4QFp(mQL(^^{H-uG zC7l=+cg>0~{LbbW=d1N&RT|YpppWC-52PzUN29D4j%ZGm+yV~#DdV2E;-wowQ4sO| zxl95HAP5q+!TUk-#YaQye&PJ#$01NYz;W|{okU8;b0MM3*MA(rXXo}->#74Z==W46 z+QIDWw_K%B!r)HQ{Ur-5(GkfOKBFE7So`<0+22ww0~ye|(mIAM?G=Y2Nb(Y@5uXGX8aB<5q`6r`M<@y6} zuE0U)<08nIt_mR}NT~lymYKdP&)8RCS>T^3ZSg(@ojS&;#S2j`Vluj9G%qI@^Yf-n zd;mrMmNhf&l3Wf2%k@9q(HOiX&X6dx_w|1jvEyc2X%x6NJpl3qrCa3(<+37KefCIlS53Cyw|;p$OSul{Siy zHTxD%txIoH82e9PKV~TK#?z>VFA9K97>2B-LHfSK_400QTkXDo88THy`Q7(69laq^uQw?ug!Hg}qi!9_Q3L&+1sK&ra9CtoYR%hD6f;sMVD{O{OrS@At zZ(QRC(x~^6w&){~zwh=jd!|V6`e_NC0x1Byy3NcNygsv(4qsnEm?g?X4nb)ALH*&a zNUV&`&_l)*e@(`#sHsU^;Nk2b0PlLPaF_4{|9oa|hVW`kS}0^a2_64hnLB`Hl$KqP zGiq?!9MJ{YG8_%Zd}2VGc1t$(q3c4Xb!|l-m>!Mm(KD7&&MMQ-D;O*G(tMH-JCU=p z8!>pVI<2J4^PKQ9XWXUOf{L$431?F#p#_k|g8Z?v71c>kPw1+Ox7{0p{IvMD4DWmn z1zFbN38bg|iDOwo3*JpM@PmtHw49BdCk)tlYI4ucK@u`JO&7&7LuwrM_nUc=p$)xK z5Yzi9gWUZ=4fA=FJ-)U(;>W}XuUWR={cAJW*%Kt%*?zX&st%K$!gSMkb`84gKr_}p#%wSGu;w(vX zV^q*fV%Dn@3xX|?H4j|1+d;|78|751W)tBF0XE`ae+07h;xNr;Z3fhclYo;B zG1TNarCs=sD;g#LNLrsR!K77TUn8Xcu(bzqvlE(>a4S*<_8i!~>krAYc>~2_0ODdm zP|C}Mg7CdMb#a^h6zOnh(3-zE$rCwadCzhyq}@&^@{*GViV>L911!r zqd0c_&Ory;?S7&v4=NhM4IjfhDw0m*6p6tay_KDGqlbUvi$}ZH40!XYSYxL4gkw=k z`-yxgQB09`X^rp*FtVgbIX#c7^qhDKM*Dpp-JaqcdT{zb9tg=e2H3_=f_h1`9|o(w zR*O{a;AuZ({~QUQP_O-J38vx4?pIq8k2t-0ApkSNE4+m#qCUWx3k%dRntb z&3;g0+5O%;Pz1(vv+9+t^~=+YA1@CY_Jr!6g|RhlQZq9!WVnXB*dI?tDo`^YA3eA@ zxk*{LMx{)l0QEJ!rJIUPdJe|CK$GU{i{x0V?w&{bJ%rj0y_N@{WgwFbD)!dzD;}a6 zBhxsouc-}N;Ys0qE+y=Qt{QmIYbwSTX+8Xb#7J4VnW~C34s=${_%_{@Y9G+30E+5N=;5DUwVE3emf=0%ee7?eMUFM8NU7m2@a@^d_Dc5;eZ!c=q_oDCod>C< z1e4)CV;9f%>mTah1R4)-ynT-*?!=t2ywo2Haq%?^P%UMC&@_XwVFal7r&d7{iE77I`F!hpjWTx5j8elg8{mse^x1V3w};;U$_Bx=}9rKLO5deLO^pG-RVO_ zD-k#1)B|WwRt)$L*I~Fj;D-AI;x(9Fa0d)i*Xc4ju^)WCSdo2gQvC7heqDN&wRXWXDA>*dfrFdFe`M8Yc`G<5_S$!xys5;yr*W&$(fa_5x0T6*mJVg6!Pf zaZx~aTOl{%*)3m&Sz#Yc`#vDYmhYj()0S3W)b4BZoT=r_P;sI&1gl-9DYc9-ATYJ@ zf~P-9oUeFW6G|nhD#Gi991>d!&hb?^VXxl4lQ?A17BjEmY(M#Kc69D-{@iJ&Z3*~t z&NA55T7C0@>Tl|r_M@gTS-dRm0wOj4#DZyRX7`9)x`r|BxYGOmrsDEJHI5?kkupMk z>)U3U3KXhP7h?a%zVhD90zJe4Ba0xemcD2D_hLaB%|?30sdajo(%7c>^o|+w98pCj zQ*}H0&xRQvDlf#Uf?9lo8t=8T33Y$WVOenDPgEl=1kKo zGJvo-yA>&L`0-TNi}$@?nkPvX*-X{kgsXA#w`SV_g+K;EsM}W@b)aSXQ#UQS%5!oR zs-~aqcGH*o4)e`s#Hv88Ni4w%9_g51yOtwfOH0_N8PON4M^XWyu6ee> z*)CZi)ihehHf)-oHj=E4o`t>gXxxm6!hJ@B3N}(2sHZl^X=go0-N=QZNNVsg&*z3V zz(=s};iPVXyQ+Mx0o|hJGk1KQ!BNHO=zm9WfZ0DuC|DHY!4x0VgHvHxr(<4{v5YJK zQn~c@AHZH3<;8n#Smlc=meCTXyZ`Ls5XwOuqyH|$W3T;c{1I?f#u(_7DXb*qRVS(?mvqR(!1AR!BNt_jR{p1 zd%t(4Wml{6Rt4HsWf&!fIhi`%x#g4NLlqai^TRtkau)PrdwgXG<+r zzf|(sdNGDHs;cf4#;He!H^mSP{feEq>9>&lXJ9>AQuCbRS~?Mh=#Kj*kmy{PmEgLk z@AQBs?dL*Glo?T~N{jQvS$9x|M3%r^xxX_?Di(P$o5_y|u+zQ()yu>E2N#+7u%CCR?#HE#m zkrd*eJ*KoxY<$9W3#2xaRMKZ6nC6yz>wdpJz1ojPe5|hK{`svF=`&A^tb_ddF;d?P zP5J>?-MG%0jfn9vOuv0k>(xdYKkz1{)y_UKkCSh!9UI&A;Pk(&sF&;iWVP>tAPv}^ zc^YT!6?y+SH}Z!O`RL05-t7jg!{>Dsyzj^ZfuQZ!&qqdwp^>%8^S(ES9Vcp$n|v+i ztaFw!@~QCzp{x=yZQ}i7ahX!ul$(dW*$_S<{*Wm{XNDJHQ_B_mEXKKAIP}3$H_Wv* zFXa3+zct^_syp7V5Lf=@yYSt|K>sjaLpK@-;~7cP`he%Gr#}XQ$}c~jwLXC>Yyo3k zhF$*cHu{b$0=cc$Gr^(~t8T|W0WsdJqH~D*;x9>vosAiISene?pkBOT;{416esVK5 zv@Otyh(hdx<3tUI0D%3cZ2eF?_n5f!?G3HTK%&2@t559iIikyJK+KVmeJGc zbr0OCK@WWO*sJHoZ;7XQQ6XcB`WJ!gkAd2w-Om@%0O)J)DXEUDgBk7assvhh(TD{9 z$D~mKw0FS%{}%=(Wv^o>)~AkYR7$; zzvDT(x$*M86%Pq&vp^`c|9SE7>8)2e>{uY3Ut580_&fgCT_a*>>kp*y$6iXlGcMl? zQfwi&9qY_I;=MkZ0zmI3Xg@oKXZ_@`N8n{GqM+yWYtNzL@!@YjnHyyf(Wqe=tDB~b z{OE$Gctpo#XhPr=X%}X5NKrrMO8w1;hf$2iq43+Ga5vjZ_02{`UQog7$CT62DECu{ zvdEfOy_JJ*NJfDNLEqzzp-}TDorvEmOV4fc5^zP!9uOh2@Ui0)@W!AYaC+)#3UYbM z*(4qF?2l;IZ^u5)clz$V4dxo?^S<>z3(~t<3tC!H+>6u7i!yrut+kgS>I&-*j4%q9 z0IzYP{$k*Fg~FbqR>6OFZEl+T-^(D*EmrgL@%Pj`E=RhS{d%6yns5HI*N+6z9z-KQ zI%#`Rx4W>X1>XFKibvu>W{<1?@%@bcu;XA$4fx-FMBk@}pLVqRq0TiE_LW8N8lJcR zV+HuUox6LQ^+n{h)!;;91l0eczx6Or;(Jux&Bo0clVQsbUEg_tr0=sp^R^Z6!jZq_ z^Vh4sdW^OI@O#fuG-rd&oXOjJMuAwrOwZLntGx{Wi|@G}NA7P66(ruOE-FIHWujn6 zKz^8H0PA!!&c)(0@f+`4E0aVDQ7gY69raQul2Wa?!|GL}=k%=E@hZL5^WyYa8paCF zI%z;~$5q7sb9CvcHDTMV;nt(~f#Pla+$g$z*AeC8)h+=Ng2zESjMxX|5ZC{@AgADc z+=^rrcIS@a?dE(IfZP9pMY82Y*1Jkj_cE;&!Jn|%|BQq}oHeRg&qo;WG!eljh}Lsj z#p(23R&i*9dJU+5TQIn^@_jLRJMuBiGW56$*XZ~+OZ-&Z-_!rRAt8vmC+yIT^|{(z z2ROd=TlBr&HyFO|y$pA`@d*Ic&USA1m|%kyL&ZS|iSI$cPHyQS}@F$$fzz714dp3it>E5{@%^UE9W6w|#p) zCI$3`sEuy+eJm~k9RhBDy{!?>4m-Rrzdwyzk!Ia>J$T+NTkUgeB7|zVc=aRejemhPG zn0mV?qU3x^0f)5Y|7`dUS_l0R+zRGts8`u!c*AMSas3n`FsGjQ;{#UI zMD%bk$@*=gg5)X_5L|M6~Z`(ls zdKY$5J|RDa>-&%b&q8(*i15*2uT7(mBhED+M+_~41}DS#C&d!b-D|*S?WpG;(BoLi z_#w>$iqSaHG=D`*(cFvN$8?!vKJznND6Lm56FuJfey``lKRw5P&xB5MkA9x(^2!qR zrYS_boyTmCM10ZJv%8V~z3-cA&Swm{ole|sqF!ivytRXW@0t3VDYJyZ76NvOn{Th3 zB}<;>t3@|1pxD9|#5uzZ!{Zlu9<#$Pc;A=0JU#*LP2cw=qwT0`?`58zhpyU=hrSA@ zL7npg5a-*Pi!sJ)L36}i^PD30?j5Yhd#{@2T(4r&c{IdpLC$EpnpRo=-S2MqgG=7r zkX>-~;ij{@U}PKY@cLvxCeGfD3fIn`E^6fae$Vned$5K+EG&Y@>bBZ~;fD1Os1;T) zce;$YxxFZJ#CfA&B_4ey{uj!^=kp$u9TLpP{LZiaf~L7Z%6Ftn2;Q(LGU@Ylq5-#{ zAe-ESPy6+7E3O@h6ln!_`0;^kC^IGL|HK%!h8C+Ohtt0UvdHR*{{K8AJn?T%M zFwBi>KXinQsoh+au>G1a`;6~D?G`!v(7SVo1?T==7g>0g$oc&Fah0?)x)o>O6L0M7{WI`9 zcg%M*JL>9L*wOqa-AdhAe}Dja z2MJgH2RH!jgFSE9-JAflR{rWrz<+*+O_0&^xc~Ew-A^-Pwa@Vo0rL4Q5#)MLc06x1>Vx+4U>m_NVUaBy&DI8v*M*Gv ziOTcK<2-1q!}`lt)g2YfmImuRG`%5x!mq`Zay-9f|s?n=QCtLAFVH z+Z{Gw&#KSkJQh!F4%>6Mp;XVYU4|swulVy7`FhKN_v-6DzUga02ZGt_J9@|)s(g58 zEKx8aNtu~l7>S35GGeURt}#K+im7ar-y zYCS+S;0|Bt?m{&-PGhah8oAE%)LOgU8)^4_NBIkc`?Y8^b4A_I^~YOW$9v8k(z#e2 zA0*+b^4^^2=g-0$L$?p<+*T*#|F9*$4a+vfJtDu1|b#s>F6z~{5O9+2Xbu$O=7(+C=%S^>k!(Wy4$WlvPn=let{aLDO) zdhV<^*zLRf<@-;sTcW6(B;fNO?Twm4CmN~eT=mVfPyR>^TwVXYYb4Sx;0U?aBT*RO>d~M=vaN52h_CMczor2dz8s`uS%&2zL3v4u- z+dmQmJ||2E{)zH1ja=XTTx)*n1LF%;(QB?;_sRK?ZNELbD{n_sa0KU^;r=fa{6pQ2 zD)^X}Y1zWF(`xSqzC^(*2Q@`ld6H1e|Z?Su%z1V?7<3b|9=}B-@Hs}XR3ANmj z+4mjU`tDX61zVvi?#qK85bgO#{`UO&TrTxa0*)G}duNEe`)L`rqc7ZtnYsCZM*~>k zzOqQ<-1Z$+pozg)6;K4c8Rn;=)m|*Kcl&|8Igxha=8!L759D(5NH{zAF}(D2CgF1v z(JmDFdBeGyRUo`_-gj*V+VgSXgZvOu8+qA6omeNFMy=bF78JGDbJ^f;~s>imDPgB zth7eD9=<$ls{Y=#_NY@93%xw3V{6XTyYIgfet1*tA&0?~<*%3&Rd9|04NOw+=m6TF zk`Eu8zk*gBb>{2rbhURowKc1of9QcgEEChV;7o6vM#(xLO> z|F75&a6ZC@{DGv452(yg9YrJnQ<12OHT^S1Lk~{gIm@Og>dj6!$xUiU!AMTp)8(|~ zPFuWWoSYjLS!uOax{I$PTP&ofbTR?Tp1QP7yy?+ae(aPoMSQ~t?4�@&vGx$)8aD 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�b605!`gQ! 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 literal 0 HcmV?d00001 diff --git a/src/www/static/img/login-pane.xcf b/src/www/static/img/login-pane.xcf new file mode 100644 index 0000000000000000000000000000000000000000..c95d52c735f10afa76643d500366119ae6b3c651 GIT binary patch literal 157678 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ⅇ;l;oJeMEh|GFnBBs82bLcEPQtC{udC$E`4#O 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

{9oevhs(s)Qp3NxgE8pH^Tp}vs7 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 fyYT1++^Tu3cb9xV-vE}Q ztSW*tg|obQ-9%MM)08AhNzxP%!QJWgdaSLz;MU@8g85O~A#@V1U9Zz@HHo4KaB#=W z7z_rqcXpVats^Sg^1mo>cif%1`WzdZo5XPpK$0Y^u0E$)tzu@F8X-&+OdY#9X%8$^IF9i{2^oPCz(G~$bUKvE70wk3BuT=hN{Lph$^86{ za-!Gt&ODO{r4l;9ws&$J|jf zOr7cJQ(V4smG$+v%*>q5MrQ=0AEQUxpFZB%`mlwGOiZIn?2xEkQer34&_#=aX zf*^y6I2S{1-!2B^FoC<^|9_@w2hTnHE-lQ(aO3GGhVAE`Gi*NnjN!_oj|~6+Gca6y z@PXmS-~S9vsdfzi8U7tGPMgWaz`(%33^Rb!D(S?hm#;%VeEz{}s4B{;ry$6nA;ZVO z#=^w#`qNJa0|h|_FOHB~eZc=Mw}bsZ0Lvzw+|`shh9=o1`xJk3SZ009GajhOC~QIBjOc jq6ftV5Je<7o9+MrkSnX>jNIhm00000NkvXXu0mjfni~0U literal 0 HcmV?d00001 diff --git a/src/www/static/img/mail-inbox-16.png b/src/www/static/img/mail-inbox-16.png new file mode 100644 index 0000000000000000000000000000000000000000..274d2c30e73e8a628594001e85cac7079a130e01 GIT binary patch literal 476 zcmV<20VDp2P)u` zKVTf4a$Co2h2~oE-rnyxhzSv#vi4oh3y1TZ!#PJ(m0@Its{RiEz&@Idj~nx&IeVc> z!MThqb2#V7vTPt4V{Qwh-kWN*dNly>CtfsOznUsk-dCROrmFnd-!J^SjvBxmfT$|l z+n?Y0q2HLFTOgOq{WeAt5u@O64nR>=R5hh*(Z5rZ*;h+vFCr0<_<+#8E}R~SY^f8sp|k&@2H S!u(bM0000$GJA*Vo3y){aEuwS4j z8|Y1?BsUKg)KlEVV+8+Xk~ZDVcE&^8O|~h~OFi{v-r&FQAzx9pASL{{ZAsSH^R#(@` zA;(c8R!Yy#fl`V}r8?q{5Hg|_MFAlMmSti7@>SFuwGx1G;J8>UEJTg?>pQpaqSTaF#~&*x1f-Z3x(ni1pumQ>)FgD7={7YbulrK zgyXny90#uJ!f{-)}W1doU>))3Ln#9A9cS2mrtFbvB#Dp>r6jnVOu2 zX_^DDX`0|29$M1zv4{2nhjnoZrv*P~g-!|p@D7J*#)rXiv^yM{ri}o`-CQTHwRWc! zcAQ8gf9{rdCjrub2>Fx4dXVEn(;~kDFk{gR0FvVhhR^i4AOtM{_zl9X3vq__!>s@S N002ovPDHLkV1gE+EjR!G literal 0 HcmV?d00001 diff --git a/src/www/static/img/mail-reply-all-16.png b/src/www/static/img/mail-reply-all-16.png new file mode 100644 index 0000000000000000000000000000000000000000..768be24f828d3daa75d47567d01aea40bcc35358 GIT binary patch literal 586 zcmV-Q0=4~#P)`6pHR5*>jl1)fdQ5c4wbMDc(cO=mq zoh+i5Xp;tox+td;L@@I-73#1_=Oan=< z8<^F2@66nLPm7viypD@DeQ;Lid(U}z-xvOI^kwAqWAn(;d*WJSsPMu>hCUfE29P|1 z2;aX<5Q}=&xoFeLnRkyy3bK~CcSvtY5cuq=v)Yr&>KH6JoKm&Kp7XY*_{m)&b&3mZ ziGeLfq_f@g5GpKozuiJL3?L1QY}#3aBBbHRb|#BWTaQV^CWJt%YO!Nq#rb*#v&E)o za2${!2H)_UoMq2mCziN`QaGv_+p(_!0V@kB=ANZc3U=?SA`m8SVHZDF+GsW6luATp zAnAW`U!bfmtkz-jza(=Xpm#{)dC-`cs+@-HPg%AjY~HpPA^bm&R(sHSbd20+KV_mf z4-XOeP8EScJaI7C(Up=QdHtYUS8|@ra9#x5DSM%VJoSyoLq7Nfg^!e0Cz16%Opl&j z5&%>8h`JH-<)44d9{^oi7o!j+hP5BF z?Du85*HkrJrmzPHe8XVD4;X1Sx!G}o;yTQU41L)&T5GMdX*TZk-w8K`!>1W9NBdvq YHwmTZdKgRg%>V!Z07*qoM6N<$f<5&Ing9R* literal 0 HcmV?d00001 diff --git a/src/www/static/img/mail-reply-sender-16.png b/src/www/static/img/mail-reply-sender-16.png new file mode 100644 index 0000000000000000000000000000000000000000..240c60e5f3cda0fd352cd073317f71a02959fda2 GIT binary patch literal 466 zcmV;@0WJQCP)zl1nRuVHk$5_kBN14rL~Z z6eSijvmn_`Is5<%J7sLh(xEKuWMN^!#t%TT5M`&3LW?BrB)W7rF38}(?`0BWXF2OM5dkeTi1%yav; zueX+zOb5HW$GB9w+N{7#~ zlLsp#os}rTl`#G2ijFZHMvVAkKP0U$_eW(Qo#iNRGnbq5PuIu$!@_Q01!ht1CS;i} z)xe4q51n&mu0Guz2M%-csTCC z+cP@0F``XB+D@m-n`6xs>j)b5Mq9A5Tt8ufG_U;KC;q>@09_xL5i;mpsQ>@~07*qo IM6N<$f^-(hfB*mh literal 0 HcmV?d00001 diff --git a/src/www/static/img/message-16.gif b/src/www/static/img/message-16.gif new file mode 100644 index 0000000000000000000000000000000000000000..781cf62be1e1190ccb3c57ebd122d63a0adf02b4 GIT binary patch literal 356 zcmV-q0h|6uNk%w1VGsZi0Hr$s0001piiMDkhn19$l97?2pqizkouQzhrlp~%r=hB; zr?0H3udb}IuBx)JuC}zZxwx~uySKi*xxKx;!N0o2#l^+9?6?CbFF?eg&K@bK{S z^YZug@%Q!e_x1Dj_4W7n_xSem`S#}s CcF4p4 literal 0 HcmV?d00001 diff --git a/src/www/static/img/message-16.png b/src/www/static/img/message-16.png new file mode 100644 index 0000000000000000000000000000000000000000..e4b653e25cb13a61dc463cc0f3c4077e2dc68c7f GIT binary patch literal 771 zcmV+e1N{7nP)V)-q9!RQyL)+&>hWzC;II~z_UM-w3#GJ;M(v)Sj#@d>H0Lj0x-t@p zghy7__L2eG?G2o0+?bh}3BP^!=IqN?FP@z^ezIu-PiN0v9F98C{a@Es&(Rn5L9+bZqQEa&3JTDI}qgg%pzQO_%2P7PjSZbov-m zlapk#S>7xykxpfVZQC{wkw6?EY=sH@4vilT&?bq*ehTBm*mmTP8^_s)6cPZ{RZ&36 z-(hYvT6kVSCY>b~i(y%zJvm4zaU5sx?Dr-+mgW0;olZBP-EPrtxAA=+Q<&Hhn|M4< zquC&x$qWdXfBG~GQt46Z^*YO+K9kAhn3_65K3@RCU~zGgSS*eZyA%z^fCB)nHOWMR ziHXAmK|oh^5mHbn6v&U~S^2txGZe)zz+kto0fDW}Ee?+7*>Sf~TBDUhDTUGsfLta= ztNjy_>Ae}a+b*e83g7q9^vGJHbx%YoeB~3dZTja=>fSIUM^pWe>AR%Sy7#?$t6>;u ztqnlSZnyhj?#aWOL(b5i+Wv>FtxY`NdtNG*y!8Txy!0znr?iZAS zftgCBSzTHCk-WaSqFQ}0fr30J=-C|Sr87LwrRluVZZ&WlHf`6T>DatHKT)%i`kV7)JclRvm2AUS{G0Nxlv`GzP(ctpJLMVON_XW#_ zfvTzj|Ca!Spk9AuePgo^cRT;h{ii`_0M(_7i^Pe}Dt6iKzD_6Oua3WJ4BsvtIE=MUIsQ=6~^f)NrFm8ehhc5aHD z*}SZ4np_wdhI7t6_gn`4vt;^VkmrjA8^Ed)Sr!qT$SH6P75JsQa(G65N2X51FV+Vl1Gw-bRuo-YQoPc_x~ zn)j$Gn#l6^lx9Wv@acrQa30{HPV%w8|5pbdi&r*}YZvodTZ1Ac8 literal 0 HcmV?d00001 diff --git a/src/www/static/img/msg-gradient.png b/src/www/static/img/msg-gradient.png new file mode 100644 index 0000000000000000000000000000000000000000..55b31ab655f4ff54656911f201704788a9974183 GIT binary patch literal 206 zcmeAS@N?(olHy`uVBq!ia0vp^j6lr8!3HE3cV7<&Qfx`y?k)`fL2$v|<&%LToCO|{ z#S9GG!XV7ZFl&wkP>{XE)7O>#8l$wZDfdi|RjYwQvL&t&CC>S|xv6<249-QVi6yBi z3gww4844j8sS56%z5(x3RP%rejXYf(LpZJ{@A#K{yk3nB2-dI|GfOiIvkTiZ3yW`7 q01}fKm=Djd-~Z3p(7?cmf#D-R`^#?+WD0-1T4uiEtKp zL>4nJaCd?*qxs3xYk`97C7!;n>{q!t1dUAICWku$g{FGCIEF}EZk=Gr*KEM!Dt~05 zG;{Iwd25%y*jgp|?tn%mw`8zop7Beac7MgH-De0|?-1AOYP;*K;LsY^s--3rCyE0#VTc2$4 z=kja=xvrk@2w~Gl#>-3}F&ZtK)N!ro-kO>Vzw^&^*D)M#va(WI+baunAcLo?pUXO@ GgeCyL>0YY< literal 0 HcmV?d00001 diff --git a/src/www/static/img/network-server.png b/src/www/static/img/network-server.png new file mode 100644 index 0000000000000000000000000000000000000000..4f863f99c46c9f4a39c2c21cd2deced312fe242b GIT binary patch literal 1202 zcmV;j1Wo&iP)G`9S688Fd1SL0ETA z;9zG?_75o2i7AYX+(ACCZ5SK?0s%k5;gc9R^^y{gKYY>6)|CJug6dZk#72hE*4~C- zZLKE)E9o@8iw)!4**8Impl+D}0Ei-mZs-Ws)?y<5$P=d4pH2=TpL>eh1xqZ zFfib1J2W&@=Ch*&CMPGo0ed9idJQ)VwiTaEXCNMr|NjI;mmXlunTU)rFvbvz#maoP zO+YFEOC%C&K{@B1e6~%X^fZ=bc>>X#mcqdReO>29LC(u%#9U>rMabXF6 z{qZ;afdKY*Jcp@&W~$on+y5*mmDYY=xH>k>41V^bw|f$0*p~w2KGiHTzg5$8h04*-%V=lzO5CCKs${rX-~C7eE)*NxqIod-1= z24~y`fF?iz04h;D)ABSfd~gwxM6s|mkG781swF7;6y?uFflC6&mp}1UCj)x0CSX2Q zBT4l%^~2%&==|+L8jYVzT8?IPgO+UFMZjq&#w;Y~r~f;pYxwm|_~Vo?&TRX=(72}y4R$U2w?y!ez$%6$t3{JohBs_4c&lG_CePT$Q5+arzn@+>glXT)YCGO zs02dD+LO)Z9G%Hl)PxX7CAYv$Ja5Bw1PXyNRWu=FN7k{jMnI%+s_r~1D_H;p*-WmY z2FT-+P(c-OBT!$Q`;TXUG3KgEGf=>&-z)(cfLa6)L}#!R!2x$IQ3^pMrHe}~V3tX> zLWuw?<{0Q&p;Ts6QE2&kByOMrl63UMm_L8(T-0$|S0B%_0ax6Z%* z+8c6XV-p18sb^tXCKgij7#zH%&(0>J0L%hrnYhi9CRJ5;zx&=h7whZl&e)9Ac#2*G z7DzU|oc`vsPcMI%%jJ>)<~B=!!f75E9=B!%u>j}*mav5k6h+!50n^RkUn(io3b)Ns Q_y7O^07*qoM6N<$f(beyY5)KL literal 0 HcmV?d00001 diff --git a/src/www/static/img/open-read.png b/src/www/static/img/open-read.png new file mode 100644 index 0000000000000000000000000000000000000000..857dce967155970dd57b8887e4086b5959ba43b7 GIT binary patch literal 216 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4u_bxCyD&^*xWW*yIgeQ!D8gCb z5n0T@z%2~Ij105pNB{-dOFVsD*{?CO@(XCc*H^Fs3dxqZMwB?`=jNv7l`uFLr6!i7 zrYMwWmSiY|WTYy%d-?{vPf^VSDzx!*aSV~ToSd-0Ea6Vgqj&X5B1VF_O3Xl@7+kA6 zMN%?RCZ-F-c&PR$h$py3G{>&+yZuJFB|Hoa4^6~)&K^BC6=)8Fr>mdKI;Vst0M?N@ A2mk;8 literal 0 HcmV?d00001 diff --git a/src/www/static/img/open-unread.png b/src/www/static/img/open-unread.png new file mode 100644 index 0000000000000000000000000000000000000000..0490dbb4df7716fbe1a37d962b64fcc73db9269b GIT binary patch literal 216 zcmeAS@N?(olHy`uVBq!ia0vp^AT}2V8<6ZZI=>f4u_bxCyDx`7I;J! zGca%qgD@k*tT_@uLG}_)Usv{PjI8_uT#MZdl7K?8C9V-A&iT2ysd*&~&PAz-C8;S2 z<(VZJ3LzP(3hti10q;{(^MDF%JY5_^BrYc>EHF!$Q=vM^j!6DxxOC^?ZXwLwt+96vlt&+#6#8 zb#<_l6({XK;UEUwT zW5VWQ6@XW@vKJ5^g8`kN5!0tH_!0!J2HAm}k*=|e#JC!UM;@{U80$mNrDVpQ>P~VO za4wc@3(NpT3m}F`_2kVb)jOkFU%0_uACz%`TE>CACK)7~^>2euy|dPXkqD0gUe?M< z2_f^F59K>?T%WI@Jpjl5x`DqsoB%!bP26#gc4}enHRo|Rp!4lB?||#1n)zLpZGmcN zW~NrjY$I1qo*%979%v?;7#(u{XqKj0W<;@adxSo7>jxW0E9?Q6K-Z)tHtL*>PEoUY zR16P97;%pNphu&7#s0-9Pd{$)$&*|6{>IcFDFS)GdaMUrdtyI4{0)5>2d N002ovPDHLkV1l$A+w%Yb literal 0 HcmV?d00001 diff --git a/src/www/static/img/popup.png b/src/www/static/img/popup.png new file mode 100644 index 0000000000000000000000000000000000000000..8ad14498cd4a9e0467ab7862f41d24e9cd57c2bb GIT binary patch literal 6777 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 literal 0 HcmV?d00001 diff --git a/src/www/static/img/sad.png b/src/www/static/img/sad.png new file mode 100644 index 0000000000000000000000000000000000000000..9eeca4f034ac9893e05ead1e46b6e15dfc5a2641 GIT binary patch literal 13166 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 z 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%0Dmgft}U_h+P5k z^dP54+uypu;hmoc`8=)r8PFeD{P|lVFJjzra(q7eEATLMm_HIQ=V&lSIGDO!<~K( zu!;gNoMxt=?oy-(Ug`n6U_qmf27#xl`_$P6khg@I>j;yRENy6=5>8W!0$(R8#jzN z$|4SkiaMj$@Sa{_BN|U#A$2Lo^e0g+Ho&l29ny1y6CKuyP|g9tt3+RB5F?(rHNym=f6bNa{R^OyY!_ zTpNQ=sh>@lIS4GZ{Y6!ro)i^(72FXK8^XY!xx8RhjkjVsm$-$Ue>5PRL3ayD7;U2Ue zw>5J;XvWcKUqiseqGZ31Z46G1tbkV;K>gUV3_Kqk2r}L|MGkp={Dh|gat*k0OEj4m ztaWmpeK3j}IVp9MWW#V~kK%QJ7TC_^yjQI5D0wtpD|Z?{2{Io{lG)nJaWoLjkT$#; z3~}p3q$CgCW)XzQfIK+}F<(h3snWWU#{@`(;o1`eDs`k@neuS6r|}g>TRAtL8an1V zRI$3n-t=0aW7qUU6!Fl;O{w+XnOBit%V!>~V(`{q@X>+&Jh5Z9xUD=HQ{jM6+wcf; zx~vGpgL#T@z(gT`_bS^6!;tO}5wmur>durW`^5ib4fCvd`LcOcO>p=Cg|*EJra;Kp zW;fPpG?P;K4+gVVFBfPcU&3K8PRBZTJ=&y70o>1N0CfN1KuafarbVTEDW~)#gU-tye-AxioA8k764Zj*U(lJf>j}M$Wnb>#(+q1I zo**`e%sA1{iO}J}ZJdx0Ci*;JGObhrW)5{ohv(D;adKYL&v)zP$a}&zlqlS8Xl9M6 zwIT<{bdTf=n<9pk%bXa?)WFd6L)!^(I8Mau5B=2Gd~0kGAtTs`6(ponNzT5@%j)nH zO>vo*ZJ9CK$6;cMc;UxmK0Y(ZS&E206~r*I^-B{Xmg4p!v3?5K=@uUd2=R0Z(<~AC zy?M1ppAP~l4S^ORp+aG|Z79~;%Ch*}m{BAB48#teM5HjrBGXt`y-b}uh~2E(X@ShT;$gm!g=QEocY*jDURQN15^9E0}8!v+u|;@?KWwBOa-bOg_~Tsvm2 zbGcmn;Qpih6y^Jl3?ir4`s8T_CCl3R>hKV2yrvy00QAKBUJ`mm|uuqD^U-H zfk^!w`;>6@7s3ObtrNFcsfvUh$&sk$uwHSu@~+A$lMCr>0uE6po51y=+%@!D$KDxF zv7^H|ukJt<;uU_G@>L$vebd4C<#c>;*%zVjK!o3Tqe?@^yFIsDmoo`@&^_f?H(EFE zVEEAL-jDWLSSGR2UBnm=0n^bR3j{6$rLkR_HznKe0v)0Lg`8&U=H74*g#k@Vh~tRD zgrc!iLycuDR!Y+8*V+){lAE8h^m<(p+0sRGbuxxotOmm>VeF;zXxc=Kp{ywP1H1Q9 zMBrZGq0u>vRrd?=HbGCQ6*O0LH9VpuVzKK%@gE}UFt zB4DGX_~0Y0nulp}iY6!EbGBS80OI_P-}Kn3FV|Sj>T_%#Spn-HOL?R>HQIC(-e7 z?y9j_t)A{;xW>KUL-KPRVV1ga3b)YmfsJP-p2iS7yaqiI>y(h0R#705auF|0C?-;7 zCGCDE1Yuy8;Q)7@%1Bzn`AU#ylZ6pr2Q+@)C4~! z-KdH+MGq+pjKLY5=!`zKLDZqF$#WnPf~Yyi4O?hD^xWMpkhos1H-tGXTK$ob=V=Sd zp^;9*5?|gEPz1cn!^y+(`SnO#3dWNO%lm6zr@*$ZBA`HLE@MXUz1YV;ZIQFtwkJ$S z%cuqB8c)Zc5H2i+K<4>>1r8L*aHB2zMb;sL`o_eoAmW^!w!Gm;9wh%86Nj53hZ{Z3 z>$Y2T?`78_mt!X#u!exqFvMYbOp0#N`uz6voMXCjTh7!?r6dM8*&%-hzFXs6Z z40es0N7(yJ*}bWO%#rlX$Qws#1g2$UtbG*z(5!S)d;b=h1uJ1``aCYfI;Te7a-xbI z*B7UGx9JNs^aE_R8N;C~BjA4jqdi$M2(SSLqaRtGUw~k^-mUKVEFmUAcWW++C)w$P z`mDXg7DY4X{ddEXmx|@S7EGwl4mV8O*T#i76PSKn?T&Kx4PD z5qPpor>bTFm&}6Q34JMnb=JRozne@lS%9Ta7l%CN6}$tZ-c_dn=*}{qFhytdIi7rb zcSWw+)9*o`CzGY86+68_Wbb&Jt&(_eS&s6=Nr2a7{kjLj zc|u;(^yX^3P!Zp0gCK}cErO0Z)kc~!e=S=K<9)!)RtzT(eHzat18}UJ zs`7a`qEeh)S>hgI_<&EGFQe1Jxd>C9TOCKP8!l=KeH`!|>`)Dttiy|JudBbD@nayo zhG|2c001BWNkl{vqp_GytfG>^xK5r92rKx4!bQK@iHeRv0fK4a&& zAU&K#>6tuXy@8*6SkIS?x@&;8QVX@YBsozp&9Sp?zPINj5f{ zZ3v`GXpY;(4BesJ3&M!j@sK!THM@bDJkyGg8ePMan{jNgcGp3^33rHtzVZFvUTQsL@u56sXN)J5#f(}9Ev&R8sT*pSitX`A|}r^Ye}?B-u;qd^b?~|7016FmC$ZFi=KR&apme%&uu5AuQLR$ zL4|m^;%Azy0QI7}m_SEyG*dYYVEJ87S@0wx6`XyG#KC@) zba9gg7p7%U>iUxd-`o!-B9UdD!TP=N23oDnZmK7zNbc3UCp$^sWl$tcF6WJScCSXz zZpaB1t-wqJt%OybHD*ZkUz^o2N2p4F}#sBs%10z8MO!N#@Do*n`bqx`!)|R z^+O|KHI|jMW)jP)h}E%K_7e9L_hG?$_x&S3xhw%A&ZMjH3vss+1$FZ)~^T|1m3iDMVBAp;=H-W|h?OZ!44Ic;}0Q#Ore@`JnuT4c0+i&Y1KRZLV03Msg={m*egv| zkVdjzK3JEdR--|xZy0xKAW^nMO?wQagLlad+#QD^Z%3t7+5w?bDG+=1ogN&fWOi2A z_EF}_iuQRycvlP$<9$3F&6exU<^dWGu-)=#_~4gwhHUNUVSwS($tH(sh@&2pX&=OB zgPp+ll(aEuUj4)_|G7^w&SnS5j;AIkY|ljCP+BHgwaT|ELD1jfmS8_Mnhhd*u;L$; zFL@7nrmjxT3K%wzm%$@}MLy*Hj1QMVsBzjOW2nV(u#>`qPW0nAfEXPd(jA5mxFOwM z9(BICsm;Q}OUaKw{dho{RM{w#d~>%%`l6j|l0J$_P&fC(kaSS-(uPN)M68Z|cI07n zjml%3A}-XSN-(l!%wk+BlX+fO+SV{Fq{pm6UN%;HHpt1#u<>BP?|sW`;1*RD)8N zMRH}Xc7%5nLqOSfehyu;@?*J~X=p+?xL)pYY^#$d*|XGTyjhe>@+jxANmzFoNys-4 zbfV1gf?3jKl9{U0Q#W~Eo*Zc@sgsWzBZ$WNFWhdF)j0_%WWm++d^!i)Ma88M(fCG7 zBZ06y6X?ai0d$n6Ljqz(AYI(I64J1(G6 zh0H0GM8f_ImZq6W_L9aF$dC@Q3<*MKC0W0fhR;;oyY3-HMHtZzI9B)TWF@%8ri15c ziEAWmefHe*Z*T~2mK_cQof572L(C=Snhdi0u!W^?NrRFEtQi~C6ua1EPC5S-=uj9K zb)Cvz5VMynki6`KaWW4FG-5TX75xHY8Z}rHAnF!ch4@)34EB3_S1auDNOzV z3MYXcbuu46`;zB|;L7y-o8CrGw%HZvCZqFt{AF4+92i((ywU8R==r)lSP9WO`YBwY znqyO9vi&V#&<1e$$QYBN=KsZkAl?m}IZc}wczF%?V}Jn1@i98nZ}hZ?V4nmc$Z0%Q zO+KSq+ec@k;qQ`sR<(l?o2hwqQNox8lp;rOQw1rQGMX|1G6jQy-722S0bi}dgX944 zQW;ncBFTV?(>5`<|A966t(B5S(9cPCXzZ*z?~icp-|Y61M_m`sBfByGk@G1xaeI-( zA_VGFmMcqk&V~(5SO){VYWOX?*`2*iZ%Y1`|9xGl#jOkDWKN->3+e%@ey-~eVxlH^ zpB>LYk0dokN07G#FGgm?>2<^H^3@Bs&6&(>1inRqXIG+lQ6W1&Pi8AJK*G(Cqh^{p zqsrO!*d%xibpktX8!q!^vN;-Nv5h7?%jsBbKJZcG9h?ng9cd_cKy&obsc)j_5lI`r+ey}^9Gf3K>o24Gl^f zHWjFm^csW1SsdcH0weh+F5%u7qUvGZrNs-WPZ+@LpC+lF;0X`)xJYhNF0YX0|OdGlsV8 zUWo3BnYMU1^3{6yXudLRuM1k#7ABBRU(9Lb{w5_!do4LaRA4nRq-{Q8(}ZRWGO6%9 z)1rS0bZ9WkGj-q*yDY|J5zT@TIpg2bcP@WbkEUntb&<_A+xK>)4)n){#@VFVE+xq% z8@6LgEnKhLRcp#^nA>I6l$M*$i5RV{Fd75&YhX>fVkgHSu(;OCo{UbIAZ5B`&d`}6 z6$pNdPPZ6l24J5*Bb?O}?qox=b9p6JJ1=%G2Iq&5{NR-O zP8+JXTzefFo5njB2rQj5Gj#gHQ^2^vjPeu)n$J2dx`99y1e>GJb^3`-KGa5LI6N{8 zF9kS~LPz%}9ALZY-VfKaaNgaVvi~yFKpye=cpZXKI-zEM30kmnJRA;)M7tqErCW?l zO|r(J3W(0!iQi?q$&R6xdyil<<65M_;Bsgl2PHgn%d%iwXrq5uPl4Q0dRcH&&PE%e zOb3A)5T?{3HMb9`02CoFPlcemyjR5gGKUo0zNpG1Qo>8RA#&J3tn6WVY-=nY_3?rZ z91dENbi*n0ZKXTe>+gi)q|PS1?Mbol>vSBSk{%^T+SAH`vH+|np*;y|{6vA>QLM%3bnG*Jv8e`v^#{6^9YnoGNWc*D5Y1}x|r zdQ5>9`>;t|Fg>70;KIyJpw}bW#F__~V^~zxCo@CK8AG$lGt~A_!XZzgpaGWI^^ZgF zfstjuub~&yM38oKAT&t3#^$KwhWu1VMUFwE!BxoMkQTeQj?E5rsRu?$BMRek8HPIE zbyaaI#=-zV(LTp|K2RQaL?kHYPXvglj%66KHfkHp90AIz&JphXD1=(iq#!8|6;-k{F#Bt-SxNxeKg<%U+&4< z!g(v|4sStGv#IQ4UxX30>lTeqWSRIGn8Mx4WF*YVO|jn0du!mRA_2%`pQW`Fg5{<( zF0^DICE%RA6xvJ2bokkY-W$rqu%1$xK`IqyENBs2JW>fm&D(0^*LE1bcf6u z&Spl0E3*?+hzo1y#gHig3zmEIJ5}?$@D%X%H1qbSc+r}m8Mw4Z7bOP>2TZ1Z3(4KJ zmq;(nNE>wPCW{!YWd>+Wvzh`CQB4gHmPsx5jP3}8gb}Nvi4IDtE*XU84*mYOPEH*_ zk<%@9#AW1229*x{z`&wUUPiT&<*>oSstJyDE2rEE3)@a;Ol2)wF5W_x;nSrPo`_he zNG%)3m)frt9WS-8Zqe9j9}C%Wb)kWMzMV1EQ31@*$Ky#0(BwJ z=YtwgsZ(2mv_TV#0cQ3u`@2j zOQ9%Ike~~7p$j**T8clQE|fwy7A>^3P_k+GxXAb2J2PkQ`{upxd+(le?wnQH@B4n= zy>n*f%*>hjd>|oTWn`fN!4+x{oD#Q{iN&{IhnLxaxJVeT9Mjm82my8SYpMv?8PqH- zl1`)_eHYxd@W=z%C((w1AkF%jp)FA|TQYR<5d+N%@&r(TCx$D`@{}pyk*ulQCklaK zkGHd8(hI{D17TZc^2kr$0>X{IhsE!m@;D22EV3hs*)z-r1rLK_*w`BFcuL zHuZGM^tz@Z!+`E$hczI%9hb^(05R)r$m<6n5Bf(S1DXk5a)%vt?UK<~Gr@Trl0(MB zbR0qTD_KotX&y5Kpl;$e&-dL{A9(FGx=!seR{A8UX7DtdRF7W^|(zmjo{W-ZQ zi&#L?fB4B3GUnlYwreQM9VRZpK*9){9Dq#?RoAThtL}kdb+~uU?aazqc5_F7fkG0g zEoRWAY8>7t-n^Z5d zh!7H7GB+>Xw=WOxRzn27Cb&^;ZZK4>Ha_t{KRN5x8g855ymV*#WII6X9*zlf71$cb zP?wRt=N98k4ukrT5$H47$D{;1+;*75Nk@jQKBMaqhdJlN0d~J?q%AXg8X)}vT?9CzR*)S#tv7FV& zjyh=-qF>KHBCm|I@gA6VdI=mO0jsH}1cL=DWaF5?!uw~0AU?1+KxszM%(7-8klXdl zRqhR02J37J^uXM}z9+gSg$2c;d}YZDX9JsT0)3V&L9#{-%{`X&`$K1%JR8Z8!;((J zbtg1vvS~7uPyk%qfNsJKBdaxYc8Ak(wHYj8$V(BYk9Xdz0!fTI zkQySLk$Ho)gWq%_3SUk4#)==p zsqN~UWcZZ7@S{K`@79|YU|ma40ZfLTgZlAj%F`IQ(c`Uiplz%&iOI!+M=k4rM&5X# zAeKA^CFHf$flFY~`hct%T2Zih3Yc*&!|eknryeQ`?QRW&oq7mNZb}P~`8ZyevGeCV z9F+WG&IjT4+ex%%lTA1o7!75J2HArt+%&Jc2m^26v<1Lkh8dlc1FA_kYB4#NX3lV> z#|}KN@x!T!4mNU#DG0AJQA_>l|yyiyp544XXL>^9}8z)Z*Fey8k{Vs0oNiw zTgxTWx;peOFyI|pDmvF6pw7Vn`jtZ0p$xReul9JH6zXjE6I&yfL=NRp-T_49`f-}m zsN)v%oq;zXD^|OT;tcH#6Lr8DS!buN%8r`NDGdR`2JK?}Thc%8*kKL9uR7fZf=1F= zy+4cN&Fav6>h`Q5#5R@~uWXxy$YG<(h#Qv#K7b z3w@PM*-gzsH8Nys!@@aKj|Gz^3Dg;adymMXV9wyI+@6=S$V%W|Qc0{cf0^!g!fNNir2|6vF3hu&<{x1?yoWR;46yBr+6uiN!qVoh|DN8Im(+tgn!}_Cr}Z)E=K@ z?NuQvLeYPyW+@ifW*xPMqy*?E9@rYnjJ2mTGB$PnoQRr<`x?B!;Lw1d+d%G9uKFeWm zY)E-OR+F?Us}_VTv$7kD@*UJL;`S87w(>AA+=j!V9x6~a*(r~}baAmP-9b+hcI7XD z)Tqr|U;K2}5~_OChfu7z(ge1e7-t9TSwszx&7vj_Q5F@B15jt3#G8}wD-0cT&v5d1 z8>Okc#1YPGFBCF}o>=Qo3n-8`SoA+=Y*prrSjUD=ol>*FM}sX`3LXf1$pMg}uxECr zhbd>ORD|u&F#IrbAmQzy`Z4iGySPwU4M?Pnhgs=L zbvz~)Zd{{S&*mh8P~HX?medv0^_m>Y+u-80{~p>hgHYZukK&zTC~vG-p}etL$j{~| zFB*|1R`{J04Jf%-jn)sbw~FA2*Yw#4CM%N%Hy$>isy`bmV&3(82oAe0S$PpsZ~Sq& zF`=a@UXANgqX?ImkUj58c_YgRzh0N@qt08MYH~;8t?no_^)|64<-t=^Zxd@$D-yO?Ydl4<)PalAloy5mtyt?kMR}H(FRXy@#mX zg31XSf*YaygbJ(FTy98ti!#M<^Vgv6*r?$YuDo38F2$OtkXDnhRpAMz=C0BF_owL? z`PAGsH~;-nDvMR^X*JQ7$XG3$Rw?vzDq%F5`}VYy%|*6fp&WQrP2^1~Z>%^PN;le2 z-Z&bnU8w~}5Nnz%g(H|wB!E!f@SwFu0vyU49yHv1Q(*&QpPGCTo`Ui=;EJ0$O{Q}! zrg>Hyq3wh`KB`w;00`BY zMMcVsM#L3qR!K&diWe@%6Ivi+mThEd8t=<%`K>r5#^tC$vI4tA7Btm!3eT!$)iZKfDU!Nz0W z+8r3jlT256W5p_3dE-Ef9Cd7kSXz1GKx-vuuQ$Spuwk;Uvi{>Xl(z|2i4ng$ls8sO zTD2jUAIcl6TH_4mja9ApO$9$&tk{>i-nSg(joMp;(+gTpYTR67HEcbpQF9G@oPb}6 zgzZ}Qv=w{gc8n{OcUxc)a*w5rSjf1ru=2JZ!g7S^v#SG&R!=1X3&{qxwy zlS+9|F4VfO{toZ@Yq%&OtVUV67UhkqK?E$N=|CiGv8r`gX*ZVGSn?XHvi(XeSz?4b zbz3#BQ+dPJH6oEltU85MVukXC_bbet5y~4`bG&sE%G<0hBVMX`nS&DOzGyWtq&*TsirWCH<5?D;lKe(w|w-L4<@5!QHE~Dj<>e9DBXP1 zP5&-WfT3mURyeL)p${E8NC{Uy{7am25f#+7A4iYeO6kqF-ui8*MXXxRp4sVe=N%`A zufP8K{kPxssSmDPxx$-#h?4BQtf#3rq}h}VQ%dyfU;cv9<;#~Z-*@jluWmNxDoa&( zp)HCv_Ku!@@BzAR-@dDL-Z$QS^GDaRIpLkglt871vxdih=xuJ^_Ob8mAD3Qw z>4*2HjDHl+-&*Drk z|J`^ZGQaTnWAyNszm$7p@54WS;f1d~aO%{rb}w2qY67LY@AeYUuYToWdghzo@ZT15 zK;)jec=4w%zxvvR$>%juj2J~k7ao6XP;qxuULv9=o_z8<|NPe*-`ni<@v1~OmgcSL znJO@(RXk!!iJpJ%SvOQzNO_5fPMkP#_vtfdU;N`Mf4V*VOxbDyECp&=j0K&#=OkTx z`l&2ATUvQ%>yh*4zjfn{ANlrQ|MvHfg)+rj$e>~ z{OrrG{P|?L;l$VuGJ@ruoOViyPCxhnJ#_X=cjdcKdb*7A&es0@`}cqL+_}dOA3S)D z_W@i0qyOyvWFU8GbZ8({Xk{QrNlj4iWF>9@00FE?L_t(|+U=V$N<%>u zMeh}Bf80^F5kbTzm1q%{p&;5>DHe93JIKziMgB~FQi_46oKt*4%5ZYu+?n~-$miF` zVm6#EhuO(|7zWLTnE$BH9*6qFV*(IZ+WQe|n;} zr^!*MIQpZ>8kOxSs4A)x^^;RG?xAv`c0(EbP_j&^7k>v$q4Mb0_CiW%`cSj=kNf>@ zSNzZviYBVEDO7Sc#fskrs)^c;`CF2P)(;g$%|z18{2wD0pjzus#=Cyocm=4~`loXK zwD*(3a9~lR-`JKSiQ$lC6g=(T+U){V%x$obE$qzM0zb?)%ZzY{6T3dg3k;*^p-|`Iu Wg?ybW9LY5R0000DxO@5r zyiZZhV_;z1?&;zfQgQ3;ZNt9H4h*ap?A?<39h{wAd0AC;O~oGwT?x4o)5N`G*;&Q~ zth02SoT4}dSeR~pRa@q$&7ySbmYi0 zRc%dI4WM~@nLgQg+UZ=^^pxTSn!h5#VV;_0-S>ad`|9$8y*jy=7#Kn{6~4c}S26X; z#kAW`zurC{x%Re6FIYlD!9ISzO69hw`Olut_P4b8HR`wr92_cmLG0aff?BYuWSe8qW!bKXN%ZK1xwwmOSCW_D^ia^zGLaITbFi+Um>HBeccr zV&m`ZXWuv&7*}-PIPCv7xPNK+He*9Et;?r09OL;muxl|a-e13jWdV!U`#0bP0l+XkKKUZg? literal 0 HcmV?d00001 diff --git a/src/www/static/img/side-cl.png b/src/www/static/img/side-cl.png new file mode 100644 index 0000000000000000000000000000000000000000..d2790504bbba3a4c130624727b8685fc85bdc0b3 GIT binary patch literal 271 zcmeAS@N?(olHy`uVBq!ia0y~yU{(OKB{-OXWbb9ZBp}6E;1OBOz`!jG!i)^F=12eq z*-JcqUD>a(N{FZ_MdauF0}9EOxJHyX=jZ08=9Mrw7o{eaq^2m8XO?6rgk+>DxO@5r zyiZZh11fCwba4!+xb^m$AtQr;2*X8xe%p`$uhn?0X_ z;X(8?28II(j0_Sw3=GYz3=IdF7z`p97}&VM+=LAb3_N0BE=h{!aqFKal_c literal 0 HcmV?d00001 diff --git a/src/www/static/img/side-cr.png b/src/www/static/img/side-cr.png new file mode 100644 index 0000000000000000000000000000000000000000..c044344a1a2559fbd7f0e7e2695744f6be60e1e3 GIT binary patch literal 187 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjoCO|{#S9GG!XV7ZFl!D-1$&97 zuPggCRtaHKeWyD25TKB3iEBiObAE1aYF-J0b5UwyNotBhd1gt5LP$ocg1e`0!21-{ zJfK2DPZ!4!kK@TTpI)7=XOn8UY#wK900M>=BzmF@vJS3bT`UG?1h6wOgjTcFZ}8c) Q5U82K)78&qol`;+0Q9aiMF0Q* literal 0 HcmV?d00001 diff --git a/src/www/static/img/side-tl.png b/src/www/static/img/side-tl.png new file mode 100644 index 0000000000000000000000000000000000000000..40a3fa0f7ef564d0aea996988df3916db3e74d9c GIT binary patch literal 500 zcmeAS@N?(olHy`uVBq!ia0y~yU{(OKB{}%WQgQ3;UF$rlK!(_sf6Y{CW*2;kW9{!>y~&sI@1Dzs}XRV_<05%zC1B z(w%!IFMr?LsLjCeU=5>@|D=ZtPNiLsWC6M}yFtbC)heIcmCu0Du*0EYitEH{clm+p zgcv6ssRtQwnw0}cG5`%{L1rj807D%lf}PRu|BC*tP{(KOLW1Xk3K=|I{an^LB{Ts5 Dmxi!z literal 0 HcmV?d00001 diff --git a/src/www/static/img/side-tr.png b/src/www/static/img/side-tr.png new file mode 100644 index 0000000000000000000000000000000000000000..c7131b5158172f68e063312fc8a784caf830e0ec GIT binary patch literal 384 zcmeAS@N?(olHy`uVBq!ia0vp^5+KaM1|%Pp+x`GjoCO|{#S9GG!XV7ZFl!D-1$&97 zuPggCRtXUundFBv?*oNoOI#yLobz*YQ}ap~oQqNuOHxx5$}>wc6hbmm72G|21Ky{o z<^dIc^K@|x@i_i=f}tN%por`F8x(z25UR-<{a>i*v)iE?M{Q~TJDA@ zX)fO$F#&-dW99#mJoY{tlxl>!cV6B7UG@Ew*vbkS&D}3$C775W*mW*m{wc_Y;gnaS z^@8a~CbCXg|NalZgJ_5->lLkOQ{$h8b=R!)nx#8aw@B^tl#e>4>c0+@3b}8KE#NMB`Zal9v8&SS^UJ$?o=;!7`t_b?i+?QIm6Rvf zx<#7p0E4-yjd#Q4~c%F@5L>*83WMXDa_y zx8M3A+tRpUpm~kEKCS;vp{;E@Xgp|XKL^^x;B7kj_5^5?xH}WTha(-GAD!kDn>h<~ zWKMn$cYZwvx+M0`MrT(w^*JAyGk%!6f%(*zZ{6g#j&%Ew88{{6@HOB{qFl+8 zLrD>0n|+XUmH=1QsN)0WK4NzaljE5bkxROZfJZ}mv}44mo3Ks0h9}$>F!^w5oS5*8 zOnU~$C)^WLp5Np&1-z60LFkkp%^$*{2DC=v+^a}WWB zDb=_L6^oIQO9Fuq!B7+vUgU7gIpQ)*TcbreB7}2WYkY(ua6x`uS&a!6m2w}7uh-s_ z2@pdkDuNYC!y~j_@k}ceK0sf}VN?LKU|2BFBIClNvzf-3w2VOUKXGZ z+iBQ=R;=qYDt%>3+lr(-zbihU8g=?@8oya3?54y3mOqUzJQ2PpbjIxKTR*y* zRG0V+W=k>KUu`t)E@dK#VK0jo1wrgQU37vvfgK(bSRdA^V9*kN?JsE0p+|Z%ba5)* zUV3o!#+X>YnRMm*h>Z5&QZX-oZbX)h5wujM2$(PQMz5kN<}8j*c|2KF&ST8j{4@?Z zFOw^ot0g0cf_!;7D{-R)(Q|8wdCMd^QD9^?y}$b`o?d*p*nj@>eYwc_K56Leu)X%w wLe^0|y#Z~yRnIa>;u*3rnvZXk6rx9j=rkeB5K~-00TN2{@fmt03W3)D0a0M}5dZ)H literal 0 HcmV?d00001 diff --git a/src/www/static/img/star-widget.png b/src/www/static/img/star-widget.png new file mode 100644 index 0000000000000000000000000000000000000000..6dd9c68107c4fd06d2e1b893648643393170e438 GIT binary patch literal 1576 zcmV+@2G{wCP)9fA@XN%=3)L_85$DEs7Chfh}7xZlbzkg?9;?iqeLxVUq=m?3_&& zQLOGDs#K|IN!1Tfsnu3VQKS~Csx?UW!IjZSuUb`G|; zw-0)rH~CCuWipvmE|;qrV~p0?tgf!s(&=>SsVc~3vySI^Q^{n~4#N5r*+K{` z%R&f&5MtYCSr$qu6NX_~YrSX<4Gq+3-fXxp}*c(XBP``&e3eBU?OY&O5K zu`xb0G*sUX-o1ObKarY5Z zX-cJ%xq0*EPyX5SuWcCJ9o}*S%KEN-xx!oP*BBU(G&`WYmVa-lgZ?XuC6X8nM?{HL`OcK?*#I# zCUN@y{rj8QY<7BPW@h}#l`F@`#>P(hzTcb6V$*E6kdmo{shQ^7q^L}rsUFaYaZn1o;#b`+kFxk0QL}dCDCtt`0sBW^Z>|tpGG4Z z_{Ftq#a@<@%Y9#;KQHcGA3d_SSKWy8J|960frx?n9CQJ!28a+0fDPIOyADB_)uqpZ zK#6e+Sd_k9Caf5ogAmSuRR${s90akeX*Lo>0Zbn_$^d0x(b|7>uHI0LQe0Z6{!@)L z0@~k7vIf*aZOxv$5BQr|S@=Bvvx|xG{YU5O+g&Js{9J!~DtU{I4-rBHqw$_7(`u+~&v0Sa^tbeVT9B;I>u{!$wo ziZLw5LaAoN?PRVmoH&!CYzxCZep8|RpKCE0=`DjW0@i%S%FFdGTJ=qG8Wkq2{UQfM;lI^fX=;9{Ic~CEg)QcK70f`jg(w)!)r%#`5%>!SFegX~EoSOZ6)%@c|=a}i)kMoZlAY)=05)98Pf a2mb)F;q%ZL6Z>ue0000MEOpf>8LGjs3#oi-YX3PpXl=bZ2P^8KCz{|L2JVeE}l zq73Vqh!6(;(LK%n>}Gw&Rs^==w&i<=>I=N4ddG3v{z}%&kmt<}S1bpMof0hEmFYom zd14^u{gHy%Po38Xdh)CAT{$urjt)+ z-wlKFW23NV3i1A*4M5?N+4 zc_o;iNlI@Y&&!JWSyOxNWI;)d literal 0 HcmV?d00001 diff --git a/src/www/static/img/system-installer.png b/src/www/static/img/system-installer.png new file mode 100644 index 0000000000000000000000000000000000000000..c26576ee8fcaab7c49014d9fa4b4d4c8f6a84b34 GIT binary patch literal 1589 zcmV-52Fm$~P)x~n8y}@fBP91BF!4(u01yrdhMYU8R zB~%Dmph5yjmV)|%`nYc(`iuw{fsi0VK`1S%nhMd#D5WycR?dUDget9XYvbU>-iz0B zIp_2NueD$s>{d};`jFV+SV{IIOq%y4*w;e&;RW5<;&vv`^@Jr#A303Boc}C zWMdOIZ{Eap-DMXCC{p5YcmB?%O`8Im(teuBWWE8KUJBs*e(KPnL*#Nfwrtrlcm3G0 zV;nhhWZ58^reWJQ-QC@9ER>%IAf+@+(?m*%QVIa26y4q3%PwAiR@Zf;l*U5&MH{RP zQ2srB{P=$b@G{MF0G?8pqpX7m4}K>A3p!9a^YZbD6DPhCfCU{WLsU9&;J|@J2N@X| z;lYCk3=a=eC=>vQL?WzMv4WPC7Mh!zmj_Uub>;cVlP5WP^yoA9O-)U4?b0>; zl~gK4I2=aTbsWcGa&nSew{Fqf+lysc3j)mR%ysM5rMGR{Mn0dPD-N)B?OI~77~1TP zH#RoLl`B_BrBZZsbYPk$Gcz+xPfz1G4!W*WUtdpKTN{x`gz@olHgDeic~4KzNM*f6 zd!Vupq?DXKeVWf38~H;`4Zlw&`Q0a} z!?c*n6^KM%q4CW|5={w|4paGQ@`V!FCsP!sOE`{8dwY8@7z}<;uJgi;sC*1?{``5~ z0o>MB5{U#t2rSD+2*u1~j@r7{$Ub>OZ%+>Zsr4JEscoP%_AT+c*9ZnobloE!Z_uQa zKVOmqm3`3F)dheT3=$59F-`NS{dNE$Arg)9uG&Y8qm@Hx^brQ*1Ox?pR z6&d{ZzlgnBkA{PiI*zcJ7#~%dru}iTCD1AzsC*N4?%WAL`1WmXefcGpWf2O6sH&>M zwrwKS)gU##%}kIP$S^hWm{>G`p?N4VgOUzP3cmU0{TbJF&nyNIt8}0eZFeLPM)9;c?JhDgXpN*5_SOhaSh(Ex?~)SYxXeS4uL z(13uZX~Jhp36#<> z3=gH56!Uq$?(Zw~_V#`>&+-b@c6D_f4Fm$eGEH;APpC>YH8t`6j$hL7Micp>%fw_6 z*DWBtB2tu)!bJ$l&|rp#_rDfjeDUXB-?(w(GhhTz3jt`rt3WjnVDTo^foLj~dVBl! z?Y~JTo04lg)|;=?ypE5J=N6fnp5*bPQ64_H=TAI-JbLx&)!*K|d$$+J0Hc6h2%r+g zctL&+0;uim?EFbfOUo}}vDgNsR5gH7Dxb|}`}+I)|8)KO^}hn+vym&${zU-Gl@3@u nbIPl`#gOAfZz5@&qzx;dk+WTfCV0q zllv$)FDDQGG>?#jC*%@HxkPdvk&Gvj2_!O}v{MS6MA<2kOeIjCk*N9PX9d*!LTc$N z+N0VJbVfO;rjNlWr&p9SDj8&^63^CAcm^Wh#Hweq>RGIYI#vVo1z$sMM%bKsHiu0S zAQZtcRe*3_a|&8uE|*&*gbPK(#X{qA@i4!M%WvYn6hmcgkZ^}cP$4mh#bSokTq8BK zz7cUchAO+hNZQ-#y3FlTNyA&XL)OvQ{ZZB_>+F;Xdi3nRF}@sWmP4HWFUgr8 z>Tp<{bEw1Va5%;O~W*izS$l``Fu?p_#tP^o_Z#y}O1Y%CfOh z=#a3K9(?IwWOQGxPt2hcD_Ok$fYgxGpn#3d>w4OetK5W0&*}RW)zKIqtf)Bc#<9!> z%#WfNUmWn#->-@!mW;%v-E54C43h%m>2YmZFxyxe5to^Ka`jgzjTf0vo#Ks6wS~%6&QT=#jTW>6Ic8(kNw!EYB9{P{l}TnK`jqU zXufyPq${&xVR0z-39hF^SXEql?QZ-5U^b&E95cTH!abY8KA7@-cg`n91Lyt#Tms8{ literal 0 HcmV?d00001 diff --git a/src/www/static/img/system-search.png b/src/www/static/img/system-search.png new file mode 100644 index 0000000000000000000000000000000000000000..fd7f0b07a558cb6d59af96bebb672bf1abe96c88 GIT binary patch literal 935 zcmV;Y16cftP)&ujZZ2vcpl1 zMZ@GK2apA{PG?ddXEM`!?RHyZ=kcza{{|ocz_G78)~3_xTfzOGVc*@!;z&{~D4M34 z#HCy|oAJK^O5@u;x%xm;z_nWd04$M+pY+%HrNI}599Zr)1O{&1Fr9gQ zRx^nN(=^?Ylb_Xc3crTMTm|}8W6tGCdm-gHU zoivEbfP*V-h@Y#f29hN4c>od3S|m}D80SwWrl%O%n5zs-y=aEVV!4n`6c;!WFs{rR zhNug$8$G1gD4`4AaS<H7=eg9>uBL$90I_nW$UopHtf^aCQMJwQhI406Q(#?y zO-@ZpM5e?TYiVd$*M}u5U8hd09vd6~!{hc8oX&FDU*~sLS6AD5dV3x{dHP7z^Tsz$ zm;I-vElqH5_&#iSZzKKk>qUmIhNJ7$iG2Xj914Xt0)Xc8`7QxKZ+qL(^TA-S6B++< zPs?7gTFc=3v)=)}CSYu?u#}a#&H(^gTU%RH&GN(C?nC&$dI>wJW|L@7q6Gi|002ov JPDHLkV1n1##MS@+ literal 0 HcmV?d00001 diff --git a/src/www/static/img/system-users.png b/src/www/static/img/system-users.png new file mode 100644 index 0000000000000000000000000000000000000000..749c825e0743a3d6341572de182ba101d64e2ae0 GIT binary patch literal 1901 zcmV-z2a@=SP);LPjufD2=|8-OE7BK$+UZa)$s?zGqO6geu5xXE(epV?3 zPt2bp`mX_eaj1ipqrK3K`0! zB5BXV4+4y}^uiv!PM7hq;bL0(k3Rq1k;G+KTz{?LTZh`@R5-mDyrG z3(~`JG^tdIY&Js}MQE*1N+Tv_rjjr>S~{?2-=Y2a@?PbnT#~R;>DE{8t!-Ss>-6{3 zuh(Czf9b(fm>UAo?OUixx&=kr_qQK6Cp_tjiFE( zSGn?@{Kn_|Vo<2DmBhjWhWJ$U*%kaD7 zCarddf*UYCeF#j9PGxa&W4OgBQiUo`b_DH}L8Y)^gOTxlW#u^EzafAH-Fs@Qo5hIPb5h>5@^h#0Ji z@KPSuQhZMUCr@S$YF9t~k)wx8LYqr*gdaiU@+vB_AYfe0Y}vzvZM2QCVH2ZMND?5{ zV8bRh?jUi1LNG~)QncO+fD+IA?$+B56^yOX@i!=xg)lZ`4pdk?zlBMJVkN^Sn(6vF zg;E6*Y$G;CIcX4!jeTsdfeoAJbcvu-6R~>NdTwnf|pG=msqOnm$)1xUH- zTT@fxrCR+w)`HRkR-rX$oiH@40EGy=xc*6WE+U;?VCKLq64o*P4q{@IcF>s$Dl^2z z#W#6!>45i(jeBO`(U-3UxN0w$J)C`fYHE*HZ>|wX;Jnq^7qtS`D$cInkFQSP?!O1? zuaIs26*pbLEso>n$FXh!zw5KSw7{A6&78Im=vE;2mOpUka%167XRn&`ci!B6Uokm) z?2xnY_p@ZO3fBdtL8-o@;8No__2fPd&CRj09TJ9JJ~p<@EzuvC&0Ij40IM@B^(Ogq zaGWdpFxDwi@HDTSTWlv*PA>ia+b`}m1*s$sw?w>&ByoWRV?f!r#Uoo}xp^l?jvQin zE1;JMI+f>&dxV#+jPl6+`*Bqaz|VjAH;SVZbmBCfMi*7dFniNv@%8h|r+DC32RZei zfvXEqDhSMPE-bD@d#n3U%E1^&3?vDd1eSL0pgK88vl|hFF$jnVE88I|Q)SzZapcGm z#%Jc}rl;BRUHoqUz_d~CkjrE!7IVV~dP@)eQ}mqx$+~XoyC;hls@{Az~9uoM24EQ)f3(O7YUd4k$3jAY%KmR@iR&>>aBV zu=2ItrXXpx*j!!VA+0X_bZvdTIxXpSBn+?BTki^0OLOTv#RHNR=qZCX3 z3IITW_Q2I3E+xJ8!Wfb4oI{=fYL2^FR5qxf$z00000NkvXXu0mjf)s2>+ literal 0 HcmV?d00001 diff --git a/src/www/static/img/text-x-generic.png b/src/www/static/img/text-x-generic.png new file mode 100644 index 0000000000000000000000000000000000000000..2d7f2d6017d823bf9f1209d9933faf612dffe9f8 GIT binary patch literal 333 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b z3=G`DAk4@xYmNj^kiEpy*OmP$ic-+2*wv2(<2J_t+h+ zs5rlI{^p#SO2-|aiODDi-jZP}W|aE1)~BmOR3$EA(Vi@w>U%ZDa$9qM8}c0HKK>=@ z`d4d)YqwsmShMx=n<-}wTJ%-CXPi)?Z+d)E$Kx3$zTq0>%B>G-<`ESK<06 zKLv;!Rde(>`RU=L2g~Ovo%n1M@bQ?*+>S*Vvl$k=|9v$za@qmb9gNTaiaD`NnxMxo zp5*#q&TW}{<`>WDw{ULpSLj^#x@qe3m+d`GEQ*uw+Md|dvtPsTSdE!)vF)Ukp32Aj azp#b`#_#;I)BX+6mkge+elF{r5}E*#afeO- literal 0 HcmV?d00001 diff --git a/src/www/static/img/top-gradient.png b/src/www/static/img/top-gradient.png new file mode 100644 index 0000000000000000000000000000000000000000..8779a87e5c99d0b78e608c21078acf4282e93d89 GIT binary patch literal 239 zcmeAS@N?(olHy`uVBq!ia0y~yV4MJCGjgy2$q%1?%mY$vN#5=*4F7@f-6`JoM4$*~ zfk$L91B1w6AogY4Q1GK2D9B#o>FdgVl}A{}O6b7JSIIyj*%H@?66gHf+|;}h2Ir#G z#FEq$h4Rdj426)4R0VfW-+=cis(C<#!JaOTAr_~T|NQv+yuK~aK!rh>>%F}mr`B!f zJ+(XLGG07=!lUQz|Bq6qI^Kzyn6}(G*Ae`%{!d+k%B8(eTFmbi{PcfXp%Qkm{?*Oq g>+csIJs-fvAjih{_WU0vYoPHAp00i_>zopr0N3GH)&Kwi literal 0 HcmV?d00001 diff --git a/src/www/static/img/unread-gradient.png b/src/www/static/img/unread-gradient.png new file mode 100644 index 0000000000000000000000000000000000000000..7c57c31c7f40fc33d79cf37d96be58c1f0063dd4 GIT binary patch literal 168 zcmeAS@N?(olHy`uVBq!ia0vp^j6lr6!2~3?S1^eIDb50q$YKTtZeb8+WSBKa0w~B{ z;_2(kevMIDgh41O_F4o`NVdc^qQp5rH#aq}gu%HeHL)Z$MWH;iBts!2BUQoO(>LIK zifSHEp`53SV+hCfCU^h< literal 0 HcmV?d00001 diff --git a/src/www/static/img/user-desktop.png b/src/www/static/img/user-desktop.png new file mode 100644 index 0000000000000000000000000000000000000000..da09d123573035cab5a0bb8a93d09273e31a9197 GIT binary patch literal 1372 zcmV-i1*7_jP)>YW--eBsD&0Ow>eQ`w_KNz@@td3T5jq zOJTpd_l^(1Wp`Q7G*x`?pKRvNIWu$qbLPxBvqDPAYC>66>;Dt@FAylo7xeV{tAMwF zHb6W;TfYFux;jrxtVjTOw|T?Hy^lY(8ATBYQJ6#a?U7Pi{X*YTN(P4p>iVx=`xtm8 zdmWLzP*1O4O{eco2Lb^{DwV=CO{A2ls*0j0?A!YsyLavROJjxlnM{WBpMR2(Qo6c2 zPb8M}K+EjxjN^%|tsFjd$PyZc!N}+cXV0Ex*R#73LU8=}ank8D00#~nVCT-A*7&1G zk7Al800$2q#OL#&sw(B~@}fvI(hB^z9D!}=RGPZ_hlxZZISQA{g<7OCHa13ebv47o z!spe;5%O){AvaASykR8~I3wft<+&DAFiv%<AUg=RRaEEzn5Ic|W(K>{Y5kl|C&5sV zb?d5$#-c=H5vxt6lQcJP;OdpD%*@WciYkC5?sw0}MJNU`i{{rNJ%;EnA$1ayzSn70jshWa{SF1wYxHB8gQZnyKrg+7*pVHkwNljt5Vjq4xD zDOiS-tSK!8z*C_U4Blvm#UO0ebuTbiL|U2}X=!R)QVCz}I+?TR!uae~Q51v_9Dd<> zOP*h#JRTk9Zh}Q%lUHB70oiRf6s7R{C7(oooB!>p@Bmcx^!n`!1S)jhi`{N7SpN&j zmU~Sv=b>yQlSx#y2*b#fSa~2E4r8;~{&tPOA6X$Ni^`RmXD@a?vU~{Oo3FohDnbaG zVHiWVZ%_Q=Mi($O6(yNUj(-2$51Ass4E!GGmQucLv)LM!o%H~bPNzqHyFPFlNQwD~ zF9UR-9B=}O6<4@Fn1BYPfV)5#n6MtDLI@k+0@lnAG9NGj1#kdKAPS_Ulv1p8R{sW8 e{h!gQ6ZjL&HnQOzDPki40000FVJ3sGUp-WR{}55kb?8`~ z_bh(l_MRA$S7RoFxv3}Mzb5*Bb z@qn;wMq-mn?hluWd(K_7y1dy;f+oWH;u5pNn0(Qbf4q&+_G;h+Rzzts;^Zh>xtnJh zo!t$no{5ZM2P^oE@4K}H__dOtrW&hmUY#H47r(~+^fOYi zhB+4pws7;ErN;x3D79TVuh|)5WOI(1Y&>zRGx&LPzSQq-?GB{wrdpok9S>eQx@LdI zwU^r8_kSvIGQ(PVJ_{HacFE!ktGePEwJQz=Z)!Q^S1@#;${&5sImwECNic-{M(6*Z6BEtmZT7qGAT3r`kHKGxtMN&o-=07*qoM6N<$f<01LBLDyZ literal 0 HcmV?d00001 diff --git a/src/www/static/img/x.png b/src/www/static/img/x.png new file mode 100644 index 0000000000000000000000000000000000000000..7ed40cea56a982877b74377161aef31d751182cf GIT binary patch literal 1645 zcmV-z29o)SP)WFU8GbZ8({Xk{QrNlj4iWF>9@00q%WL_t(Y$L*F~h@D3n z$A8bvGv{k})3lnTTQ>=9ZNQ3Xs~2LMgsxgJvNnWfgJ2P3O7x;)D#aj3H{eB~c5{(> z5eSulh)JuBSuOM?nx+*|l$P2UiWgOzWSg|R+2rimbIv<6j~DOR&FT6<{J8XixtMw9 z{lCBY&-2VX@c;PlLaue<@y8i8#~wGxjmA-TX}1(L@a^VlEG_4InNX5`LGZ@u~QtB^M?j@@$mXVUhsJa?}foJOyoN9)6A zdw}3O=yd~x_5k6g^90tTTIYf6_=PRP7@PayOgP~N*#S_GXlS9yP>F;DYJm`pP|bu+ z4P7z718YX^cxUnbx8D5y%6_vJOJC{s2sRN8H=4I}pQBe=U3z{MHnkGZl2sf>!yKdcvkE|KFka*n5fF%fCGN)6G3`iQ!Wx_a3bGU^+s8$iPlLpk%b z7#pSB;-&HuV{@EmYU&^j06ZcN#ZV`xQMP1gc4eDuGIU{vr_J(R%NO|9Kb_=9Pk!n{ zr+)tJ27od|@=Y{$aHa_Da^heOOA%WzvKBm*jZG%y3JqQ~FfleFAjW8Jas(I?BW2J< z)5zdT8!a1#k*DA0DPU9owu+Q+0Mmc@?Ml_!AI)sNyG7}^8tN*ggUJzG1#NZ+-HiH- zO6HKj0tOjQg(~;!AltI=%if;1B(onNWjbQ0Sm~Syq32={hXPuwaL8*lEf&Q1SD$0s zu4iAmaoxu*0cdD&_m+wG0!IYk*b}T3p_+`vlp!bJM zorbuqQf384Fq}z`E|&!&kP&3z?BPS7{QOI=I^n_JJ^!Vh=U@L}M~wMRpFaHlk=yS1 z0Wc{dCzk5)_yKOVHbc%K1>l72AWJ57GARqO;)ux+(*iNnDT)el{vvVUc8c+FQa|Id zNp2=HgzMFmktyL4MxtC)#E2$DpDD}{vm;hL#g?y?dP|PA-{CAk7Q#{vC49%>gW-bV zlHpVl4M<_QY7-e)fvh_H_y2Tpr5+F@m$`@G4ZMLfa3VMa!4UJfEQm9t8L#Rs0gg}5 zy)w@mmhhrzG!%zda0#4K#CgOOh!2Rikx)nKmyy?pk>wYU`@T*W;|&at^Z>z&;iOmL zmJzYuy9qI&L{{YJ(NfR#i+*8(%iY8opD}MZ=MYIfFpm@w=@vbZ70_%U48uqJ5AeR& z`Usa1&h#ETq&&P!gOSC)b6_4kNHWk4tUA5S!6hg6GUt_75U%_$?sHooW)65o>_5}{ zm-oSE_p{P6tBH6YD6$aMD6%7_7O^%+k!FbJhADRMCjJv6m*OXrtn{$j%3LhOhyq21 z=!}?@!V9tED7cY0K8{_}h4uRr)CN}2s5(Nez)}^<^TeQZQ + + + + + Bongo + + + + + + + + + +

+ + + diff --git a/src/www/static/login-bg-bottom.png b/src/www/static/login-bg-bottom.png new file mode 100644 index 0000000000000000000000000000000000000000..23a3c5ada67d4c2b96a0abe2dc97d042d269bb5a GIT binary patch literal 1252 zcmXApdr%Ws6vh{@A<$qYP+=7$X$S$PT_>wk)Zvu}k8Lmz$LY{Q1ZAldML-e(iv%PF zQn6hVCE8ZO2d&mr0UsTLwqOh-W5-I{0f7N5+DHg*(7~7J@w3R0?>TV`o61nxHj z$j1Zv>YHFt^p;`Ule`D0UUU|0TMK)FQm8B`Qzj@bD#||~g9Hcjcgyl|M){|*0>;jQ zd`1ekJ;R2x4?xhWR7}X*Sus2{8h0Q-68Of`_P%HE2$yoo^D zu?osX=E(l?!Sl5R<`2IfiGYWQAAa1+PktYNSUo=lH>mVkuNARg{N%rwR#n|z#<~vU z-M7&Wrqv;~a|)+@8qzy@Ls+35mDjZXtIzUO;^ZZ*Ub;EHG0EO%P%6^VF1nV+ta~Nj zS|;72kvPKt^Pxn*e2Pi_B0%wKu+@gNBc;-#VwGG?^z>T~_8Sn1Q{v=t1m-p97K3(S znr;fS(v>u3-r~75Hfux2v2p1h={qSQq=4vj?*I_EFCV+ajNG(@S zyagooM(IU^>*ud;{Q5veGePCK1X62H*&x&@-2|WIDx|0jGRRWNddYZ;L z?lKjAmZK{pE#FQ@`@ls~8hWC;EEsT*6wc90E0MV{(Fgc=C-uZDCFnQ&O zW@>JO2MCK7(?}v1VDnW(rKhrS@%Zwv4Z-Rs>tnhB%9dNmdIw$G_>wc^{DqW3@UG4) z=BQu|@fI370pcvxW$GebB-K1kPrG( zT{OHfv20@HN_RNIM@CCm!s?jxYY0+A8CS;I&UYa`^^@8d#x7?o?Hs8zZ$^CRrxGcB z^-xN2joT^rFlXN>kh6WZP7rM}D#;sNVX>Zn5|&^)F{*fK5Q1#C>~xM-uDZ|s%;wjI zT&>o`RXsO^k6OkKuhNT@G+*nh6EWQ+!`Bs4h6ezCOB<3?>6M0l-;AG-BtMy)bv-^z z(TiU0(rvuw5dqHzDK62#N^U&7W<1?;QmWI9)yT}quI!LluWew-{N5`r&tpeh3ped} RcLw}?AS_8Nyw1-(`adO;H2VMm literal 0 HcmV?d00001 diff --git a/src/www/static/login-bg-middle.png b/src/www/static/login-bg-middle.png new file mode 100644 index 0000000000000000000000000000000000000000..c4b939ca9153288b16c381d004610805c37563bd GIT binary patch literal 442 zcmV;r0Y(0aP)DPbGC#%vv>DN zS(+`slfo$10+u?d?Yy*s1U!5GYXb>*R}<`T6*%Gf(+)=gzuV!SbQL(^H=kuJ;C)T- zq^rOQ&phd{XW7G*&KhkG6OIC&2D;#AQ-MdD9d5!^=!84kP{5m3;3(i9ngDy2Nt^}z z+PczNqsEs-fG_lb kcG7?SJ&FRpN}hC(-XTX#CKG@~RsaA107*qoM6N<$g3H{s5dZ)H literal 0 HcmV?d00001 diff --git a/src/www/static/login-bg-top.png b/src/www/static/login-bg-top.png new file mode 100644 index 0000000000000000000000000000000000000000..2cf97bddc3d2b2716a4b0e610c3ed8f5466d97f6 GIT binary patch literal 921 zcmeAS@N?(olHy`uVBq!ia0vp^Gl6&~2OE&I{xG)(NU4uk z@Q4J`+`=Ht$S`Y;1W=H@#M9T6{Tc_epoUI__^!=BA=whwh!W@g+}zZ>5(ej@)Wnk1 z6ovB4k_?5Aj8p}8Pv3y|DXMu43`~DLT^vIyZoR$h?t(pSIVH9S3H-xD~wP z@gzByvN=txiH>4_H0@+x4IOltbd~t(LM%{P@>Bnnz&EV-AnoyY2U1 zPjS(GcWbVh^6eCH@%bi_Hz)AkmtlIib5G>9*y9dcPU`9JHSj$DG`!uxBE`sU)=HK8 zDdFdv+SC?TO|C6uI}$&AzAV$j33uaW`y5iVE@nIO{L^CR1Ft8&@-_P>VsfeY=Rw5; zJwc-vXP&01W@H$(@K_b^lx5}hZJXejuIKL$()+jSLfQ=PRpvJf9tm)?`yA_Ne{(*6 zvS*dveQTCOiq+3~KI~djGubwerMKWj-(1FLlRo)=OVPWSG9`c0_Hzv-C)hNVWjF2b zR}(Jo+{v=uds*J5x04r4oOjxskFnVEt6Ff%5$A7vt^%z9SzcN41Zc^dqxGGKW0Ld^ z0`biH$+B`*EGCjyXXpG>%4l&mo8kSa+$g=lTJr1r6uYk=H3wf$I_Z3E+AW@9&!0PO z3OHJvzh&upw);#>)GPeho;P`GxFguO*OOK{Z~L^8|Cx|t^SzjNyQc{5pZs$!P}kp` zcgi_tc%M&YWAj~?;QRm*-R1oUJ}++zh~#YDq=x_ zB2;elOx|d^(HT`Iw!1rX?7liqR!q4t`}Dz>MJEM}UYuJhpX}MbCU=V@(Ak`GC!Cb{ zI3c&@Rr5}u$FHkizJK!1TOnV!pK6nh_UO$#IQPu^lqJr~P8du-lb}}%RJG(^%qzn~ mEC$d#_z{?67(8A5T-G@yGyworAD$Bc literal 0 HcmV?d00001 diff --git a/src/www/static/login-logo.png b/src/www/static/login-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..919e6d031852c4eb4be78e6d4c179c1c2e2024a6 GIT binary patch literal 5110 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 Ye{Fm-DuHw+!vFvP07*qoM6N<$g471#djJ3c literal 0 HcmV?d00001 diff --git a/src/www/static/mail-inbox-22.png b/src/www/static/mail-inbox-22.png new file mode 100644 index 0000000000000000000000000000000000000000..e3a6fcf99d294e6de4840c73bba0c8c6acfaed83 GIT binary patch literal 679 zcmV;Y0$BZtP)$GJA*Vo3y){aEuwS4j z8|Y1?BsUKg)KlEVV+8+Xk~ZDVcE&^8O|~h~OFi{v-r&FQAzx9pASL{{ZAsSH^R#(@` zA;(c8R!Yy#fl`V}r8?q{5Hg|_MFAlMmSti7@>SFuwGx1G;J8>UEJTg?>pQpaqSTaF#~&*x1f-Z3x(ni1pumQ>)FgD7={7YbulrK zgyXny90#uJ!f{-)}W1doU>))3Ln#9A9cS2mrtFbvB#Dp>r6jnVOu2 zX_^DDX`0|29$M1zv4{2nhjnoZrv*P~g-!|p@D7J*#)rXiv^yM{ri}o`-CQTHwRWc! zcAQ8gf9{rdCjrub2>Fx4dXVEn(;~kDFk{gR0FvVhhR^i4AOtM{_zl9X3vq__!>s@S N002ovPDHLkV1gE+EjR!G literal 0 HcmV?d00001 diff --git a/src/www/static/top-gradient.png b/src/www/static/top-gradient.png new file mode 100644 index 0000000000000000000000000000000000000000..8779a87e5c99d0b78e608c21078acf4282e93d89 GIT binary patch literal 239 zcmeAS@N?(olHy`uVBq!ia0y~yV4MJCGjgy2$q%1?%mY$vN#5=*4F7@f-6`JoM4$*~ zfk$L91B1w6AogY4Q1GK2D9B#o>FdgVl}A{}O6b7JSIIyj*%H@?66gHf+|;}h2Ir#G z#FEq$h4Rdj426)4R0VfW-+=cis(C<#!JaOTAr_~T|NQv+yuK~aK!rh>>%F}mr`B!f zJ+(XLGG07=!lUQz|Bq6qI^Kzyn6}(G*Ae`%{!d+k%B8(eTFmbi{PcfXp%Qkm{?*Oq g>+csIJs-fvAjih{_WU0vYoPHAp00i_>zopr0N3GH)&Kwi literal 0 HcmV?d00001 diff --git a/src/www/tests/test_calendar_store.py b/src/www/tests/test_calendar_store.py new file mode 100644 index 0000000..36d5706 --- /dev/null +++ b/src/www/tests/test_calendar_store.py @@ -0,0 +1,122 @@ +import unittest + +from bongo_web.calendar_store import (CalendarNotFound, CalendarPathError, + PreconditionFailed, delete_event, + etag_matches, event_href, ical_to_json, + list_events, make_etag, parse_calendar_path, + write_event) + + +class FakeStore: + def __init__(self, documents=None): + self.documents = dict(documents or {}) + self.closed = False + + def Info(self, path): + return object() if path in self.documents else None + + def Read(self, path): + return self.documents[path] + + def Replace(self, path, document): + self.documents[path] = document + + def Write(self, collection, doc_type, document, filename=None, link=None): + self.documents["/events/" + filename] = document + + def Delete(self, path): + del self.documents[path] + + def Quit(self): + self.closed = True + + def Events(self, calendar, props, start=None, end=None): + return [] + + +class CalendarPathTests(unittest.TestCase): + user = "mario@example.test" + + def test_personal_event(self): + result = parse_calendar_path( + "/calendars/mario@example.test/personal/event-1.ics", self.user) + self.assertEqual(result["event"], "event-1.ics") + self.assertEqual(result["store_path"], "/events/event-1") + + def test_other_user_is_forbidden(self): + with self.assertRaises(PermissionError): + parse_calendar_path( + "/calendars/other@example.test/personal/event.ics", self.user) + + def test_unknown_calendar(self): + with self.assertRaises(CalendarNotFound): + parse_calendar_path( + "/calendars/mario@example.test/work/event.ics", self.user) + + def test_path_tricks_are_rejected(self): + for event in ("bad%2Fevent.ics", "bad event.ics", ".ics"): + with self.subTest(event=event), self.assertRaises(CalendarPathError): + parse_calendar_path( + f"/calendars/mario@example.test/personal/{event}", self.user) + + def test_event_href_is_encoded(self): + self.assertEqual(event_href(self.user, "event~1.ics"), + "/calendars/mario@example.test/personal/event~1.ics") + + def test_calendar_shape_checked_before_native_converter(self): + for data in (b"not a calendar", + b"BEGIN:VCALENDAR\r\nEND:VCALENDAR\r\n", + b"BEGIN:VCALENDAR\r\nBEGIN:VTODO\r\nUID:x\r\n" + b"END:VTODO\r\nEND:VCALENDAR\r\n"): + with self.subTest(data=data), self.assertRaises(ValueError): + ical_to_json(data) + + +class CalendarMutationTests(unittest.TestCase): + def factory(self, store): + return lambda user, cookie: store + + def test_create_event(self): + store = FakeStore() + created, etag = write_event( + "u", "c", "one.ics", b"ical", if_none_match="*", + client_factory=self.factory(store), to_json=lambda value: "json", + to_ical=lambda value: b"canonical") + self.assertTrue(created) + self.assertEqual(etag, make_etag(b"canonical")) + self.assertEqual(store.documents["/events/one"], "json") + self.assertTrue(store.closed) + + def test_if_none_match_rejects_overwrite(self): + store = FakeStore({"/events/one": "old"}) + with self.assertRaises(PreconditionFailed): + write_event("u", "c", "one.ics", b"ical", if_none_match="*", + client_factory=self.factory(store), + to_json=lambda value: "new", + to_ical=lambda value: b"old canonical") + + def test_if_match_controls_update_and_delete(self): + store = FakeStore({"/events/one": "old"}) + current = make_etag(b"old canonical") + self.assertTrue(etag_matches(current, current)) + created, unused = write_event( + "u", "c", "one.ics", b"ical", if_match=current, + client_factory=self.factory(store), to_json=lambda value: "new", + to_ical=lambda value: b"old canonical" if value == "old" + else b"new canonical") + self.assertFalse(created) + delete_event("u", "c", "one.ics", make_etag(b"new canonical"), + client_factory=self.factory(store), + to_ical=lambda value: b"new canonical") + self.assertNotIn("/events/one", store.documents) + + def test_empty_calendar_list(self): + store = FakeStore({"/calendars/personal": "calendar"}) + calendar = type("Calendar", (), {"uid": "123"})() + store.Info = lambda path: calendar + self.assertEqual(list_events("u", "c", + client_factory=self.factory(store)), []) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/www/tests/test_card_store.py b/src/www/tests/test_card_store.py new file mode 100644 index 0000000..425001c --- /dev/null +++ b/src/www/tests/test_card_store.py @@ -0,0 +1,98 @@ +import unittest + +from bongo_web.card_store import (CardNotFound, CardPathError, + CardPreconditionFailed, card_href, + delete_card, make_etag, parse_card_path, + vcard_to_json, write_card) + + +class FakeStore: + def __init__(self, documents=None): + self.documents = dict(documents or {}) + + def Info(self, path): + return object() if path in self.documents else None + + def Read(self, path): + return self.documents[path] + + def Replace(self, path, document): + self.documents[path] = document + + def Write(self, collection, doc_type, document, filename=None): + self.documents[collection + "/" + filename] = document + + def Delete(self, path): + del self.documents[path] + + def Quit(self): + pass + + +class CardPathTests(unittest.TestCase): + user = "mario@example.test" + + def test_personal_card(self): + result = parse_card_path( + "/addressbooks/mario@example.test/personal/contact-1.vcf", self.user) + self.assertEqual(result["card"], "contact-1.vcf") + self.assertEqual(result["store_path"], + "/addressbook/personal/contact-1") + + def test_other_user_is_forbidden(self): + with self.assertRaises(PermissionError): + parse_card_path( + "/addressbooks/other@example.test/personal/a.vcf", self.user) + + def test_unknown_addressbook(self): + with self.assertRaises(CardNotFound): + parse_card_path( + "/addressbooks/mario@example.test/work/a.vcf", self.user) + + def test_bad_card_names(self): + for card in ("bad%2Fcard.vcf", "bad card.vcf", ".vcf"): + with self.subTest(card=card), self.assertRaises(CardPathError): + parse_card_path( + f"/addressbooks/{self.user}/personal/{card}", self.user) + + def test_href(self): + self.assertEqual(card_href(self.user, "contact~1.vcf"), + "/addressbooks/mario@example.test/personal/contact~1.vcf") + + def test_shape_is_rejected_before_vobject_import(self): + for data in (b"garbage", b"BEGIN:VCARD\r\nEND:VCARD\r\n"): + with self.assertRaises(ValueError): + vcard_to_json(data) + + +class CardMutationTests(unittest.TestCase): + def factory(self, store): + return lambda user, cookie: store + + def test_create_card(self): + store = FakeStore() + created, etag = write_card( + "u", "c", "one.vcf", b"vcard", if_none_match="*", + client_factory=self.factory(store), to_json=lambda value: "json", + to_vcard=lambda value, uid: b"canonical") + self.assertTrue(created) + self.assertEqual(etag, make_etag(b"canonical")) + self.assertIn("/addressbook/personal/one", store.documents) + + def test_preconditions_and_delete(self): + path = "/addressbook/personal/one" + store = FakeStore({path: "old"}) + current = make_etag(b"old card") + with self.assertRaises(CardPreconditionFailed): + write_card("u", "c", "one.vcf", b"new", if_match='"wrong"', + client_factory=self.factory(store), + to_json=lambda value: "new", + to_vcard=lambda value, uid: b"old card") + delete_card("u", "c", "one.vcf", current, + client_factory=self.factory(store), + converter=lambda value, uid: b"old card") + self.assertNotIn(path, store.documents) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/www/tests/test_compose.py b/src/www/tests/test_compose.py new file mode 100644 index 0000000..2a4df88 --- /dev/null +++ b/src/www/tests/test_compose.py @@ -0,0 +1,83 @@ +import unittest + +from bongo_web.compose import build_message, send_message +from bongo.store.CommandStream import CommandStream + + +class FakeQueue: + def __init__(self, _user): + self.calls = [] + + def __getattr__(self, name): + return lambda *values: self.calls.append((name, *values)) + + +class FakeWire: + def __init__(self): + self.writes = [] + + def write(self, value): + if not isinstance(value, bytes): + raise TypeError("wire protocol must be bytes") + self.writes.append(value) + + def readline(self): + return b"1000 ready\r\n" + + def read(self, _length=-1): + return b"" + + def flush(self): + pass + + def close(self): + pass + + +class FakeSocket: + def __init__(self): + self.wire = FakeWire() + + def makefile(self, mode): + if mode != "rwb": + raise AssertionError("binary socket stream required") + return self.wire + + +class ComposeTests(unittest.TestCase): + def test_builds_unicode_message_without_bcc_header(self): + message, recipients = build_message( + "sender@example.test", + {"to": "Alice ", + "bcc": "hidden@example.test", "subject": "Grüße", "text": "Hallo"}) + self.assertEqual(recipients, ["alice@example.test", "hidden@example.test"]) + self.assertNotIn("Bcc", message) + self.assertIn("Grüße", str(message["Subject"])) + + def test_rejects_injection_and_missing_recipient(self): + with self.assertRaises(ValueError): + build_message("sender@example.test", {"to": "a@example.test\r\nBcc: x@y.test"}) + with self.assertRaises(ValueError): + build_message("sender@example.test", {"subject": "nobody"}) + + def test_uses_native_queue_sequence(self): + queues = [] + def factory(user): + queue = FakeQueue(user) + queues.append(queue) + return queue + send_message("sender@example.test", {"to": "to@example.test", "text": "Hi"}, factory) + names = [call[0] for call in queues[0].calls] + self.assertEqual(names, ["Create", "StoreFrom", "StoreTo", + "StoreMessage", "Run", "Quit"]) + + def test_python3_command_stream_uses_binary_protocol(self): + sock = FakeSocket() + stream = CommandStream(sock) + stream.Write("NOOP") + self.assertEqual(sock.wire.writes, [b"NOOP", b"\r\n"]) + self.assertEqual(stream.GetResponse().message, "ready") + + +if __name__ == "__main__": + unittest.main() diff --git a/src/www/tests/test_dav.py b/src/www/tests/test_dav.py new file mode 100644 index 0000000..af34e97 --- /dev/null +++ b/src/www/tests/test_dav.py @@ -0,0 +1,85 @@ +import unittest +from xml.etree import ElementTree as ET + +from bongo_web.dav import (CALDAV, CARDDAV, DAV, card_multistatus, + event_multistatus, multistatus, parse_card_report, + parse_report, qname, requested_properties) + + +class DavDiscoveryTests(unittest.TestCase): + user = "mario@example.test" + home = "/calendars/mario@example.test/" + + def test_depth_one_lists_personal_calendar(self): + root = ET.fromstring(multistatus(self.home, self.user, 1)) + responses = root.findall(f"{{{DAV}}}response") + self.assertEqual(len(responses), 2) + hrefs = [item.find(f"{{{DAV}}}href").text for item in responses] + self.assertIn(self.home + "personal/", hrefs) + + def test_calendar_root_points_to_authenticated_principal(self): + output = multistatus("/calendars/", self.user, 0) + self.assertIn(b"current-user-principal", output) + self.assertIn(b"/principals/mario@example.test/", output) + + def test_requested_missing_property_has_404_propstat(self): + body = (b'' + b'' + b'') + requested = requested_properties(body) + output = multistatus(self.home, self.user, 0, requested) + self.assertIn(b"200 OK", output) + self.assertIn(b"404 Not Found", output) + + def test_rejects_non_propfind_xml(self): + with self.assertRaises(ValueError): + requested_properties(b'') + + def test_unknown_resource_is_not_found(self): + self.assertIsNone(multistatus("/not-found/", self.user, 0)) + + def test_calendar_query_time_range(self): + report = parse_report( + b'' + b'' + b'' + b'') + self.assertEqual(report["kind"], "query") + self.assertEqual(report["start"].year, 2026) + self.assertIn(qname(CALDAV, "calendar-data"), report["properties"]) + + def test_multiget_reports_missing_href(self): + report = parse_report( + b'' + b'/calendars/u/personal/missing.ics' + b'') + output = event_multistatus([], report["properties"], report["hrefs"]) + self.assertIn(b"404 Not Found", output) + + def test_addressbook_multiget(self): + href = "/addressbooks/mario@example.test/personal/person.vcf" + report = parse_card_report( + ('' + f'{href}').encode()) + output = card_multistatus( + [{"href": href, "etag": '"etag"', + "data": b"BEGIN:VCARD\r\nEND:VCARD\r\n"}], + report["properties"], report["hrefs"]) + root = ET.fromstring(output) + self.assertIsNotNone(root.find(f".//{qname(CARDDAV, 'address-data')}")) + self.assertIn(b"BEGIN:VCARD", output) + + def test_addressbook_query(self): + report = parse_card_report( + b'' + b'') + self.assertEqual(report["kind"], "query") + + +if __name__ == "__main__": + unittest.main() diff --git a/src/www/tests/test_filters.py b/src/www/tests/test_filters.py new file mode 100644 index 0000000..0eb095c --- /dev/null +++ b/src/www/tests/test_filters.py @@ -0,0 +1,41 @@ +import unittest + +from bongo_web.filters import compile_filter, validate_filter + + +class FilterTests(unittest.TestCase): + def test_family_group_moves_to_family_folder(self): + value = {"match": "any", + "conditions": [{"type": "contact_group", "value": "Family"}], + "actions": [{"type": "move", "folder": "/mail/Family"}, + {"type": "stop"}]} + result = compile_filter(value, 42, + lambda name: ["mother@example.test", + "father@example.test"]) + self.assertTrue(result.startswith("0000002AAB")) + self.assertIn("mother@example.test", result) + self.assertIn("father@example.test", result) + self.assertIn("E006FamilyZ000Z", result) + self.assertTrue(result.endswith("F000Z000Z")) + + def test_domain_and_list_id(self): + value = {"match": "all", + "conditions": [{"type": "sender_domain", + "value": "family.example"}, + {"type": "list_id", + "value": "news.family.example"}], + "actions": [{"type": "copy", "folder": "/mail/News"}]} + result = compile_filter(value, 1) + self.assertIn("B015@family.exampleZ000ZX", result) + self.assertIn("i019news.family.exampleZ000Z", result) + + def test_rejects_unsafe_folder_and_forwarding_address(self): + for action in ({"type": "move", "folder": "/mail/../_system"}, + {"type": "forward", "address": "x\r\nBcc:y"}): + with self.assertRaises(ValueError): + validate_filter({"conditions": [{"type": "mailing_list"}], + "actions": [action]}) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/www/tests/test_html_mail.py b/src/www/tests/test_html_mail.py new file mode 100644 index 0000000..aa499d4 --- /dev/null +++ b/src/www/tests/test_html_mail.py @@ -0,0 +1,49 @@ +import unittest + +from bongo_web.html_mail import sanitize_html + + +def exercising_cleaner(html, **options): + """Exercise the URL callback without duplicating nh3's HTML parser.""" + callback = options["attribute_filter"] + values = [ + callback("img", "src", "cid:logo@example"), + callback("img", "src", "https://tracker.example/pixel.gif"), + callback("img", "src", "javascript:alert(1)"), + callback("a", "href", "https://example.test/"), + callback("a", "href", "javascript:alert(1)"), + ] + return "|".join(value or "REMOVED" for value in values) + + +class HtmlMailTests(unittest.TestCase): + def test_keeps_inline_and_marks_remote_images(self): + result = sanitize_html("

mail

", exercising_cleaner) + self.assertIn("cid:logo@example", result["html"]) + self.assertIn("bongo-remote:0", result["html"]) + self.assertIn("REMOVED", result["html"]) + self.assertEqual(result["remote_images"], + ["https://tracker.example/pixel.gif"]) + + def test_rejects_oversized_html(self): + with self.assertRaises(ValueError): + sanitize_html("x" * (4 * 1024 * 1024 + 1), exercising_cleaner) + + def test_real_sanitizer_removes_active_content_and_unsafe_css(self): + result = sanitize_html( + '

ok' + '' + '' + 'bad

') + output = result["html"] + self.assertNotIn("alert", output) + self.assertNotIn("position", output) + self.assertNotIn("background-image", output) + self.assertIn('src="cid:logo"', output) + self.assertIn('src="bongo-remote:0"', output) + self.assertEqual(result["remote_images"], + ["https://tracker.example/pixel"]) + + +if __name__ == "__main__": + unittest.main() diff --git a/src/www/tests/test_mail_store.py b/src/www/tests/test_mail_store.py new file mode 100644 index 0000000..3725f16 --- /dev/null +++ b/src/www/tests/test_mail_store.py @@ -0,0 +1,87 @@ +import unittest +from types import SimpleNamespace + +from bongo_web.mail_store import list_conversations, list_folders, read_conversation + + +class FakeStore: + def __init__(self): + self.closed = False + + def Collections(self, path): + self.assert_path = path + return [SimpleNamespace(name="/mail/Archive"), + SimpleNamespace(name="/mail/INBOX")] + + def Conversations(self, folder, props, start, end, showTotal): + item = SimpleNamespace(uid="abc", flags=2, props={ + "bongo.from": "Example User ", + "nmap.conversation.subject": "Hello", + "nmap.conversation.date": "20260715T120000Z", + "nmap.conversation.unread": "1", + "nmap.conversation.count": "2", + }) + result = iter([item]) + # StoreClient's iterator supplies total; a simple iterator exercises fallback. + return result + + def Conversation(self, uid, props=()): + return [SimpleNamespace(uid="b" * 32, flags=2, type=2)] + + def Read(self, uid): + return (b"From: Example User \r\n" + b"To: recipient@example.test\r\nSubject: Hello\r\n" + b"Content-Type: text/plain; charset=utf-8\r\n\r\nBody") + + def Quit(self): + self.closed = True + + +class MailStoreTests(unittest.TestCase): + def test_inbox_is_first(self): + store = FakeStore() + folders = list_folders("u", "c", lambda _u, _c: store) + self.assertEqual(folders[0]["path"], "/mail/INBOX") + self.assertTrue(store.closed) + + def test_conversation_values_are_structured(self): + stores = [] + def factory(_user, _cookie): + store = FakeStore() + stores.append(store) + return store + result = list_conversations("u", "c", "/mail/INBOX", 0, 30, factory) + message = result["conversations"][0] + self.assertEqual(message["subject"], "Hello") + self.assertEqual(message["from"][0]["address"], "user@example.test") + + def test_rejects_path_traversal_and_unbounded_page(self): + with self.assertRaises(ValueError): + list_conversations("u", "c", "/mail/../_system") + with self.assertRaises(ValueError): + list_conversations("u", "c", "/mail/INBOX", limit=101) + + def test_reads_plain_text_conversation(self): + store = FakeStore() + result = read_conversation("u", "c", "a" * 32, + lambda _u, _c: store) + self.assertEqual(result["messages"][0]["text"], "Body") + self.assertEqual(result["messages"][0]["subject"], "Hello") + + def test_extracts_canonical_mailing_list_id(self): + store = FakeStore() + store.Read = lambda _uid: (b"From: list@example.test\r\n" + b"List-ID: Example List \r\n" + b"List-Post: \r\n\r\nBody") + result = read_conversation("u", "c", "a" * 32, + lambda _u, _c: store) + self.assertEqual(result["messages"][0]["mailing_list"]["id"], + "list.example.test") + + def test_rejects_store_command_in_conversation_id(self): + with self.assertRaises(ValueError): + read_conversation("u", "c", "abc READ /_system") + + +if __name__ == "__main__": + unittest.main() diff --git a/src/www/tests/test_tasks.py b/src/www/tests/test_tasks.py new file mode 100644 index 0000000..1b1e571 --- /dev/null +++ b/src/www/tests/test_tasks.py @@ -0,0 +1,38 @@ +import os +import tempfile +import unittest + +from bongo_web.tasks import TaskStore + + +class TaskStoreTests(unittest.TestCase): + def setUp(self): + handle, self.path = tempfile.mkstemp() + os.close(handle) + self.tasks = TaskStore(self.path) + + def tearDown(self): + os.unlink(self.path) + + def test_tasks_are_isolated_by_user(self): + self.tasks.upsert("alice", "mailing_list", "list.example", "New list") + self.assertEqual(len(self.tasks.list("alice")), 1) + self.assertEqual(self.tasks.list("bob"), []) + + def test_disabled_task_does_not_reappear(self): + self.tasks.upsert("alice", "mailing_list", "list.example", "New list") + task = self.tasks.list("alice")[0] + self.tasks.set_state("alice", task["id"], "disabled") + self.tasks.upsert("alice", "mailing_list", "list.example", "New list") + self.assertEqual(self.tasks.list("alice"), []) + + def test_critical_task_cannot_be_dismissed(self): + self.tasks.upsert("alice", "quarantine", "42", "Malware", + severity="critical", dismissible=False) + task = self.tasks.list("alice")[0] + with self.assertRaises(PermissionError): + self.tasks.set_state("alice", task["id"], "ignored") + + +if __name__ == "__main__": + unittest.main()
+ + + +