diff --git a/.gitignore b/.gitignore
index 710ec373..c7687df8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,8 +3,10 @@ npm-debug.log
.DS_Store
config/development.*
config/production.*
+config/test.*
workers/reports/config/development.*
workers/reports/config/production.*
+workers/reports/config/test.*
dump.rdb
# generate POT file every time you want to update your PO file
diff --git a/Gruntfile.js b/Gruntfile.js
index 6ac983cc..daf89982 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -9,7 +9,7 @@ module.exports = function (grunt) {
},
nodeunit: {
- all: ['test/**/*-test.js']
+ all: ['test/nodeunit/**/*-test.js']
},
jsxgettext: {
diff --git a/app.js b/app.js
index f8bad1bf..1143df71 100644
--- a/app.js
+++ b/app.js
@@ -183,6 +183,11 @@ app.use((req, res, next) => {
res.locals.customStyles = config.customstyles || [];
res.locals.customScripts = config.customscripts || [];
+ let bodyClasses = [];
+ app.get('env') === 'test' && bodyClasses.push('page--' + (req.path.substring(1).replace(/\//g, '--') || 'home'));
+ req.user && bodyClasses.push('logged-in user-' + req.user.username);
+ res.locals.bodyClass = bodyClasses.join(' ');
+
settingsModel.list(['ua_code', 'shoutout'], (err, configItems) => {
if (err) {
return next(err);
diff --git a/config/default.toml b/config/default.toml
index 64d45fbe..af72e6dd 100644
--- a/config/default.toml
+++ b/config/default.toml
@@ -110,16 +110,6 @@ host="0.0.0.0"
# VERP hostname is in the same domain as the From address.
# disablesenderheader=true
-[testserver]
-# Starts a vanity server that redirects all mail to /dev/null
-# Mostly needed for local development
-enabled=false
-port=5587
-host="0.0.0.0"
-username="testuser"
-password="testpass"
-logger=false
-
[ldap]
# enable to use ldap user backend
enabled=false
@@ -177,3 +167,17 @@ templates=[["demo", "Demo Template"]]
# The bottom line is that if people who are creating report templates or have write access to the DB cannot be trusted,
# then it's safer to switch off the reporting functionality below.
enabled=false
+
+[testserver]
+# Starts a vanity server that redirects all mail to /dev/null
+# Mostly needed for local development
+enabled=false
+port=5587
+mailboxserverport=3001
+host="0.0.0.0"
+username="testuser"
+password="testpass"
+logger=false
+
+[seleniumwebdriver]
+browser="phantomjs"
diff --git a/lib/dbcheck.js b/lib/dbcheck.js
index 001723a1..93264800 100644
--- a/lib/dbcheck.js
+++ b/lib/dbcheck.js
@@ -107,13 +107,14 @@ function getSql(path, data, callback) {
if (err) {
return callback(err);
}
- let renderer = Handlebars.compile(source);
- return callback(null, renderer(data || {}));
+ const rendered = data ? Handlebars.compile(source)(data) : source;
+ return callback(null, rendered);
});
}
function runInitial(callback) {
- let fname = process.env.DB_FROM_START ? 'base.sql' : 'mailtrain.sql';
+ let dump = process.env.NODE_ENV === 'test' ? 'mailtrain-test.sql' : 'mailtrain.sql';
+ let fname = process.env.DB_FROM_START ? 'base.sql' : dump;
let path = pathlib.join(__dirname, '..', 'setup', 'sql', fname);
log.info('sql', 'Loading tables from %s', fname);
applyUpdate({
diff --git a/package.json b/package.json
index 7d82f7cd..ad306cd2 100644
--- a/package.json
+++ b/package.json
@@ -8,12 +8,17 @@
"test": "grunt",
"start": "node index.js",
"sqlinit": "node setup/sql/init.js",
- "sqldump": "node setup/sql/dump.js | sed -e '/^\\/\\*.*\\*\\/;$/d' -e 's/.[0-9]\\{4\\}-[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]./NOW()/g' > setup/sql/mailtrain.sql",
+ "sqldump": "node setup/sql/dump.js | sed -e '/^\\/\\*.*\\*\\/;$/d' -e 's/.[0-9]\\{4\\}-[0-9][0-9]-[0-9][0-9] [0-9][0-9]:[0-9][0-9]:[0-9][0-9]./NOW()/g' > setup/sql/mailtrain${DUMP_NAME_SUFFIX}.sql",
"sqldrop": "node setup/sql/drop.js",
"sqlgen": "npm run sqldrop && DB_FROM_START=Y npm run sqlinit && npm run sqldump",
"langs:hbs": "jsxgettext -L handlebars -k translate -o langs/hbs.pot views/layout.hbs views/index.hbs",
"langs:js": "jsxgettext -o languages/js.pot routes/index.js",
- "langs": "npm run langs:hbs && npm run langs:js"
+ "langs": "npm run langs:hbs && npm run langs:js",
+ "sqldumptest": "NODE_ENV=test DUMP_NAME_SUFFIX=-test npm run sqldump",
+ "sqlresettest": "NODE_ENV=test npm run sqldrop && NODE_ENV=test npm run sqlinit",
+ "starttest": "NODE_ENV=test node index.js",
+ "_e2e": "PATH=$PATH:./node_modules/phantomjs/lib/phantom/bin:./test/e2e/bin NODE_ENV=test ./node_modules/.bin/mocha test/e2e/index.js",
+ "e2e": "npm run sqlresettest && npm run _e2e"
},
"repository": {
"type": "git",
@@ -26,12 +31,18 @@
"node": ">=5.0.0"
},
"devDependencies": {
+ "babel-eslint": "^7.2.3",
+ "chai": "^3.5.0",
"eslint-config-nodemailer": "^1.0.0",
"grunt": "^1.0.1",
"grunt-cli": "^1.2.0",
"grunt-contrib-nodeunit": "^1.0.0",
"grunt-eslint": "^19.0.0",
- "jsxgettext-andris": "^0.9.0-patch.1"
+ "jsxgettext-andris": "^0.9.0-patch.1",
+ "mailparser": "^2.0.5",
+ "mocha": "^3.3.0",
+ "phantomjs": "^2.1.7",
+ "selenium-webdriver": "^3.4.0"
},
"optionalDependencies": {
"posix": "^4.1.1"
diff --git a/services/test-server.js b/services/test-server.js
index 025a16df..1f4777b8 100644
--- a/services/test-server.js
+++ b/services/test-server.js
@@ -4,12 +4,37 @@ let log = require('npmlog');
let config = require('config');
let crypto = require('crypto');
let humanize = require('humanize');
+let http = require('http');
let SMTPServer = require('smtp-server').SMTPServer;
+let simpleParser = require('mailparser').simpleParser;
let totalMessages = 0;
let received = 0;
+let mailstore = {
+ accounts: {},
+ saveMessage(address, message) {
+ if (!this.accounts[address]) {
+ this.accounts[address] = [];
+ }
+ this.accounts[address].push(message);
+ },
+ getMail(address, callback) {
+ if (!this.accounts[address] || this.accounts[address].length === 0) {
+ let err = new Error('No mail for ' + address);
+ err.status = 404;
+ return callback(err);
+ }
+ simpleParser(this.accounts[address].shift(), (err, mail) => {
+ if (err) {
+ return callback(err.message || err);
+ }
+ callback(null, mail);
+ });
+ }
+};
+
// Setup server
let server = new SMTPServer({
@@ -74,8 +99,12 @@ let server = new SMTPServer({
// Handle message stream
onData: (stream, session, callback) => {
let hash = crypto.createHash('md5');
+ let message = '';
stream.on('data', chunk => {
hash.update(chunk);
+ if (/^keep/i.test(session.envelope.rcptTo[0].address)) {
+ message += chunk;
+ }
});
stream.on('end', () => {
let err;
@@ -84,6 +113,12 @@ let server = new SMTPServer({
err.responseCode = 552;
return callback(err);
}
+
+ // Store message for e2e tests
+ if (/^keep/i.test(session.envelope.rcptTo[0].address)) {
+ mailstore.saveMessage(session.envelope.rcptTo[0].address, message);
+ }
+
received++;
callback(null, 'Message queued as ' + hash.digest('hex')); // accept the message once the stream is ended
});
@@ -94,6 +129,41 @@ server.on('error', err => {
log.error('Test SMTP', err.stack);
});
+let mailBoxServer = http.createServer((req, res) => {
+ let renderer = data => (
+ '
' + data.title + '' + data.body + ''
+ );
+
+ let address = req.url.substring(1);
+ mailstore.getMail(address, (err, mail) => {
+ if (err) {
+ let html = renderer({
+ title: 'error',
+ body: err.message || err
+ });
+ res.writeHead(err.status || 500, { 'Content-Type': 'text/html' });
+ return res.end(html);
+ }
+
+ let html = mail.html || renderer({
+ title: 'error',
+ body: 'This mail has no HTML part'
+ });
+
+ // https://nodemailer.com/extras/mailparser/#mail-object
+ delete mail.html;
+ delete mail.textAsHtml;
+ delete mail.attachments;
+
+ let script = '';
+ html = html.replace(/<\/body\b/i, match => script + match);
+ html = html.replace(/target="_blank"/g, 'target="_self"');
+
+ res.writeHead(200, { 'Content-Type': 'text/html' });
+ res.end(html);
+ });
+});
+
module.exports = callback => {
if (config.testserver.enabled) {
server.listen(config.testserver.port, config.testserver.host, () => {
@@ -112,7 +182,10 @@ module.exports = callback => {
}
}, 60 * 1000);
- setImmediate(callback);
+ mailBoxServer.listen(config.testserver.mailboxserverport, config.testserver.host, () => {
+ log.info('Test SMTP', 'Mail Box Server listening on port %s', config.testserver.mailboxserverport);
+ setImmediate(callback);
+ });
});
} else {
setImmediate(callback);
diff --git a/setup/sql/drop.js b/setup/sql/drop.js
index ba0291d2..3e972fb3 100644
--- a/setup/sql/drop.js
+++ b/setup/sql/drop.js
@@ -1,17 +1,23 @@
'use strict';
-if (process.env.NODE_ENV === 'production') {
- console.log('This script does not run in production'); // eslint-disable-line no-console
- process.exit(1);
-}
-
let config = require('config');
let spawn = require('child_process').spawn;
let log = require('npmlog');
let path = require('path');
+let fs = require('fs');
log.level = 'verbose';
+if (process.env.NODE_ENV === 'production') {
+ log.error('sqldrop', 'This script does not run in production');
+ process.exit(1);
+}
+
+if (process.env.NODE_ENV === 'test' && !fs.existsSync(path.join(__dirname, '..', '..', 'config', 'test.toml'))) {
+ log.error('sqldrop', 'This script only runs in test if config/test.toml (i.e. a dedicated test database) is present');
+ process.exit(1);
+}
+
function createDump(callback) {
let cmd = spawn(path.join(__dirname, 'drop.sh'), [], {
env: {
diff --git a/setup/sql/init.js b/setup/sql/init.js
index c3a638e3..c654b451 100644
--- a/setup/sql/init.js
+++ b/setup/sql/init.js
@@ -1,15 +1,22 @@
'use strict';
-if (process.env.NODE_ENV === 'production') {
- console.log('This script does not run in production'); // eslint-disable-line no-console
- process.exit(1);
-}
-
let dbcheck = require('../../lib/dbcheck');
let log = require('npmlog');
+let path = require('path');
+let fs = require('fs');
log.level = 'verbose';
+if (process.env.NODE_ENV === 'production') {
+ log.error('sqlinit', 'This script does not run in production');
+ process.exit(1);
+}
+
+if (process.env.NODE_ENV === 'test' && !fs.existsSync(path.join(__dirname, '..', '..', 'config', 'test.toml'))) {
+ log.error('sqlinit', 'This script only runs in test if config/test.toml (i.e. a dedicated test database) is present');
+ process.exit(1);
+}
+
dbcheck(err => {
if (err) {
log.error('DB', err);
diff --git a/setup/sql/mailtrain-test.sql b/setup/sql/mailtrain-test.sql
new file mode 100644
index 00000000..8ae97db9
--- /dev/null
+++ b/setup/sql/mailtrain-test.sql
@@ -0,0 +1,1023 @@
+SET UNIQUE_CHECKS=0;
+SET FOREIGN_KEY_CHECKS=0;
+
+CREATE TABLE `attachments` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `campaign` int(11) unsigned NOT NULL,
+ `filename` varchar(255) CHARACTER SET utf8mb4 NOT NULL DEFAULT '',
+ `content_type` varchar(100) CHARACTER SET ascii NOT NULL DEFAULT '',
+ `content` longblob,
+ `size` int(11) NOT NULL,
+ `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ KEY `campaign` (`campaign`),
+ CONSTRAINT `attachments_ibfk_1` FOREIGN KEY (`campaign`) REFERENCES `campaigns` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+CREATE TABLE `blacklist` (
+ `email` varchar(191) NOT NULL,
+ PRIMARY KEY (`email`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+CREATE TABLE `campaign` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `list` int(11) unsigned NOT NULL,
+ `segment` int(11) unsigned NOT NULL,
+ `subscription` int(11) unsigned NOT NULL,
+ `status` tinyint(4) unsigned NOT NULL DEFAULT '0',
+ `response` varchar(255) DEFAULT NULL,
+ `response_id` varchar(255) CHARACTER SET ascii DEFAULT NULL,
+ `updated` timestamp NULL DEFAULT NULL,
+ `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `list` (`list`,`segment`,`subscription`),
+ KEY `created` (`created`),
+ KEY `response_id` (`response_id`),
+ KEY `status_index` (`status`),
+ KEY `subscription_index` (`subscription`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+CREATE TABLE `campaign_tracker` (
+ `list` int(11) unsigned NOT NULL,
+ `subscriber` int(11) unsigned NOT NULL,
+ `link` int(11) NOT NULL,
+ `ip` varchar(100) CHARACTER SET ascii DEFAULT NULL,
+ `device_type` varchar(50) DEFAULT NULL,
+ `country` varchar(2) CHARACTER SET ascii DEFAULT NULL,
+ `count` int(11) unsigned NOT NULL DEFAULT '1',
+ `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`list`,`subscriber`,`link`),
+ KEY `created_index` (`created`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+CREATE TABLE `campaigns` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `cid` varchar(255) CHARACTER SET ascii NOT NULL,
+ `type` tinyint(4) unsigned NOT NULL DEFAULT '1',
+ `parent` int(11) unsigned DEFAULT NULL,
+ `name` varchar(255) NOT NULL DEFAULT '',
+ `description` text,
+ `list` int(11) unsigned NOT NULL,
+ `segment` int(11) unsigned DEFAULT NULL,
+ `template` int(11) unsigned NOT NULL,
+ `source_url` varchar(255) CHARACTER SET ascii DEFAULT NULL,
+ `editor_name` varchar(50) DEFAULT '',
+ `editor_data` longtext,
+ `last_check` timestamp NULL DEFAULT NULL,
+ `check_status` varchar(255) DEFAULT NULL,
+ `from` varchar(255) DEFAULT '',
+ `address` varchar(255) DEFAULT '',
+ `reply_to` varchar(255) DEFAULT '',
+ `subject` varchar(255) DEFAULT '',
+ `html` longtext,
+ `html_prepared` longtext,
+ `text` longtext,
+ `status` tinyint(4) unsigned NOT NULL DEFAULT '1',
+ `tracking_disabled` tinyint(4) unsigned NOT NULL DEFAULT '0',
+ `scheduled` timestamp NULL DEFAULT NULL,
+ `status_change` timestamp NULL DEFAULT NULL,
+ `delivered` int(11) unsigned NOT NULL DEFAULT '0',
+ `blacklisted` int(11) unsigned NOT NULL DEFAULT '0',
+ `opened` int(11) unsigned NOT NULL DEFAULT '0',
+ `clicks` int(11) unsigned NOT NULL DEFAULT '0',
+ `unsubscribed` int(11) unsigned NOT NULL DEFAULT '0',
+ `bounced` int(1) unsigned NOT NULL DEFAULT '0',
+ `complained` int(1) unsigned NOT NULL DEFAULT '0',
+ `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `cid` (`cid`),
+ KEY `name` (`name`(191)),
+ KEY `status` (`status`),
+ KEY `schedule_index` (`scheduled`),
+ KEY `type_index` (`type`),
+ KEY `parent_index` (`parent`),
+ KEY `check_index` (`last_check`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+CREATE TABLE `confirmations` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `cid` varchar(255) CHARACTER SET ascii NOT NULL,
+ `list` int(11) unsigned NOT NULL,
+ `email` varchar(255) NOT NULL,
+ `opt_in_ip` varchar(100) DEFAULT NULL,
+ `data` text NOT NULL,
+ `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `cid` (`cid`),
+ KEY `list` (`list`),
+ CONSTRAINT `confirmations_ibfk_1` FOREIGN KEY (`list`) REFERENCES `lists` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+CREATE TABLE `custom_fields` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `list` int(11) unsigned NOT NULL,
+ `name` varchar(255) DEFAULT '',
+ `key` varchar(100) CHARACTER SET ascii NOT NULL,
+ `default_value` varchar(255) DEFAULT NULL,
+ `type` varchar(255) CHARACTER SET ascii NOT NULL DEFAULT '',
+ `group` int(11) unsigned DEFAULT NULL,
+ `group_template` text,
+ `column` varchar(255) CHARACTER SET ascii DEFAULT NULL,
+ `visible` tinyint(4) unsigned NOT NULL DEFAULT '1',
+ `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `list` (`list`,`column`),
+ KEY `list_2` (`list`),
+ CONSTRAINT `custom_fields_ibfk_1` FOREIGN KEY (`list`) REFERENCES `lists` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+CREATE TABLE `custom_forms` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `list` int(11) unsigned NOT NULL,
+ `name` varchar(255) DEFAULT '',
+ `description` text,
+ `fields_shown_on_subscribe` varchar(255) DEFAULT '',
+ `fields_shown_on_manage` varchar(255) DEFAULT '',
+ `layout` longtext,
+ `form_input_style` longtext,
+ `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ KEY `list` (`list`),
+ CONSTRAINT `custom_forms_ibfk_1` FOREIGN KEY (`list`) REFERENCES `lists` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+CREATE TABLE `custom_forms_data` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `form` int(11) unsigned NOT NULL,
+ `data_key` varchar(255) DEFAULT '',
+ `data_value` longtext,
+ PRIMARY KEY (`id`),
+ KEY `form` (`form`),
+ CONSTRAINT `custom_forms_data_ibfk_1` FOREIGN KEY (`form`) REFERENCES `custom_forms` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+CREATE TABLE `import_failed` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `import` int(11) unsigned NOT NULL,
+ `email` varchar(255) NOT NULL DEFAULT '',
+ `reason` varchar(255) DEFAULT NULL,
+ `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ KEY `import` (`import`),
+ CONSTRAINT `import_failed_ibfk_1` FOREIGN KEY (`import`) REFERENCES `importer` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+CREATE TABLE `importer` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `list` int(11) unsigned NOT NULL,
+ `type` tinyint(4) unsigned NOT NULL DEFAULT '1',
+ `path` varchar(255) NOT NULL DEFAULT '',
+ `size` int(11) unsigned NOT NULL DEFAULT '0',
+ `delimiter` varchar(1) CHARACTER SET ascii NOT NULL DEFAULT ',',
+ `emailcheck` tinyint(4) unsigned NOT NULL DEFAULT '1',
+ `status` tinyint(4) unsigned NOT NULL DEFAULT '0',
+ `error` varchar(255) DEFAULT NULL,
+ `processed` int(11) unsigned NOT NULL DEFAULT '0',
+ `new` int(11) unsigned NOT NULL DEFAULT '0',
+ `failed` int(11) unsigned NOT NULL DEFAULT '0',
+ `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `mapping` text NOT NULL,
+ `finished` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `list` (`list`),
+ CONSTRAINT `importer_ibfk_1` FOREIGN KEY (`list`) REFERENCES `lists` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+CREATE TABLE `links` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `cid` varchar(255) CHARACTER SET ascii NOT NULL DEFAULT '',
+ `campaign` int(11) unsigned NOT NULL,
+ `url` varchar(255) CHARACTER SET ascii NOT NULL DEFAULT '',
+ `clicks` int(11) unsigned NOT NULL DEFAULT '0',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `cid` (`cid`),
+ UNIQUE KEY `campaign_2` (`campaign`,`url`),
+ KEY `campaign` (`campaign`),
+ CONSTRAINT `links_ibfk_1` FOREIGN KEY (`campaign`) REFERENCES `campaigns` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+CREATE TABLE `lists` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `cid` varchar(255) CHARACTER SET ascii NOT NULL,
+ `default_form` int(11) unsigned DEFAULT NULL,
+ `name` varchar(255) NOT NULL DEFAULT '',
+ `description` text,
+ `subscribers` int(11) unsigned DEFAULT '0',
+ `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `public_subscribe` tinyint(1) unsigned NOT NULL DEFAULT '1',
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `cid` (`cid`),
+ KEY `name` (`name`(191))
+) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
+INSERT INTO `lists` (`id`, `cid`, `default_form`, `name`, `description`, `subscribers`, `created`, `public_subscribe`) VALUES (1,'Hkj1vCoJb',NULL,'01 Testlist - Public Subscribe','',0,NOW(),1);
+CREATE TABLE `queued` (
+ `campaign` int(11) unsigned NOT NULL,
+ `list` int(11) unsigned NOT NULL,
+ `subscriber` int(11) unsigned NOT NULL,
+ `source` varchar(255) DEFAULT NULL,
+ `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`campaign`,`list`,`subscriber`),
+ KEY `created` (`created`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+CREATE TABLE `report_templates` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) DEFAULT '',
+ `mime_type` varchar(255) NOT NULL DEFAULT 'text/html',
+ `description` text,
+ `user_fields` longtext,
+ `js` longtext,
+ `hbs` longtext,
+ `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+CREATE TABLE `reports` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) DEFAULT '',
+ `description` text,
+ `report_template` int(11) unsigned NOT NULL,
+ `params` longtext,
+ `state` int(11) unsigned NOT NULL DEFAULT '0',
+ `last_run` datetime DEFAULT NULL,
+ `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ KEY `report_template` (`report_template`),
+ CONSTRAINT `report_template_ibfk_1` FOREIGN KEY (`report_template`) REFERENCES `report_templates` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+CREATE TABLE `rss` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `parent` int(11) unsigned NOT NULL,
+ `guid` varchar(255) NOT NULL DEFAULT '',
+ `pubdate` timestamp NULL DEFAULT NULL,
+ `campaign` int(11) unsigned DEFAULT NULL,
+ `found` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `parent_2` (`parent`,`guid`),
+ KEY `parent` (`parent`),
+ CONSTRAINT `rss_ibfk_1` FOREIGN KEY (`parent`) REFERENCES `campaigns` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+CREATE TABLE `segment_rules` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `segment` int(11) unsigned NOT NULL,
+ `column` varchar(255) CHARACTER SET ascii NOT NULL DEFAULT '',
+ `value` varchar(255) NOT NULL DEFAULT '',
+ PRIMARY KEY (`id`),
+ KEY `segment` (`segment`),
+ CONSTRAINT `segment_rules_ibfk_1` FOREIGN KEY (`segment`) REFERENCES `segments` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+CREATE TABLE `segments` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `list` int(11) unsigned NOT NULL,
+ `name` varchar(255) NOT NULL DEFAULT '',
+ `type` tinyint(4) unsigned NOT NULL,
+ `created` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ KEY `list` (`list`),
+ KEY `name` (`name`(191)),
+ CONSTRAINT `segments_ibfk_1` FOREIGN KEY (`list`) REFERENCES `lists` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+CREATE TABLE `settings` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `key` varchar(255) CHARACTER SET ascii NOT NULL DEFAULT '',
+ `value` text NOT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `key` (`key`)
+) ENGINE=InnoDB AUTO_INCREMENT=112 DEFAULT CHARSET=utf8mb4;
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (1,'smtp_hostname','localhost');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (2,'smtp_port','5587');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (3,'smtp_encryption','NONE');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (4,'smtp_user','testuser');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (5,'smtp_pass','testpass');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (6,'service_url','http://localhost:3000/');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (7,'admin_email','admin@example.com');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (8,'smtp_max_connections','5');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (9,'smtp_max_messages','100');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (10,'smtp_log','');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (11,'default_sender','My Awesome Company');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (12,'default_postaddress','1234 Main Street');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (13,'default_from','My Awesome Company');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (14,'default_address','admin@example.com');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (15,'default_subject','Test message');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (16,'default_homepage','https://mailtrain.org');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (17,'db_schema_version','27');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (46,'ua_code','');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (47,'shoutout','');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (54,'mail_transport','smtp');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (60,'ses_key','');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (61,'ses_secret','');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (62,'ses_region','us-east-1');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (65,'smtp_throttling','');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (66,'pgp_passphrase','');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (67,'pgp_private_key','');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (68,'dkim_api_key','');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (69,'dkim_domain','');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (70,'dkim_selector','');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (71,'dkim_private_key','');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (73,'smtp_self_signed','');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (74,'smtp_disable_auth','');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (75,'verp_use','');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (76,'disable_wysiwyg','');
+INSERT INTO `settings` (`id`, `key`, `value`) VALUES (77,'disable_confirmations','');
+CREATE TABLE `subscription` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `cid` varchar(255) CHARACTER SET ascii NOT NULL,
+ `email` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT '',
+ `opt_in_ip` varchar(100) DEFAULT NULL,
+ `opt_in_country` varchar(2) DEFAULT NULL,
+ `tz` varchar(100) CHARACTER SET ascii DEFAULT NULL,
+ `imported` int(11) unsigned DEFAULT NULL,
+ `status` tinyint(4) unsigned NOT NULL DEFAULT '1',
+ `is_test` tinyint(4) unsigned NOT NULL DEFAULT '0',
+ `status_change` timestamp NULL DEFAULT NULL,
+ `latest_open` timestamp NULL DEFAULT NULL,
+ `latest_click` timestamp NULL DEFAULT NULL,
+ `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `first_name` varchar(255) DEFAULT NULL,
+ `last_name` varchar(255) DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `email` (`email`),
+ UNIQUE KEY `cid` (`cid`),
+ KEY `status` (`status`),
+ KEY `first_name` (`first_name`(191)),
+ KEY `last_name` (`last_name`(191)),
+ KEY `subscriber_tz` (`tz`),
+ KEY `is_test` (`is_test`),
+ KEY `latest_open` (`latest_open`),
+ KEY `latest_click` (`latest_click`),
+ KEY `created` (`created`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+CREATE TABLE `subscription__1` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `cid` varchar(255) CHARACTER SET ascii NOT NULL,
+ `email` varchar(255) CHARACTER SET utf8 NOT NULL DEFAULT '',
+ `opt_in_ip` varchar(100) DEFAULT NULL,
+ `opt_in_country` varchar(2) DEFAULT NULL,
+ `tz` varchar(100) CHARACTER SET ascii DEFAULT NULL,
+ `imported` int(11) unsigned DEFAULT NULL,
+ `status` tinyint(4) unsigned NOT NULL DEFAULT '1',
+ `is_test` tinyint(4) unsigned NOT NULL DEFAULT '0',
+ `status_change` timestamp NULL DEFAULT NULL,
+ `latest_open` timestamp NULL DEFAULT NULL,
+ `latest_click` timestamp NULL DEFAULT NULL,
+ `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ `first_name` varchar(255) DEFAULT NULL,
+ `last_name` varchar(255) DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `email` (`email`),
+ UNIQUE KEY `cid` (`cid`),
+ KEY `status` (`status`),
+ KEY `first_name` (`first_name`(191)),
+ KEY `last_name` (`last_name`(191)),
+ KEY `subscriber_tz` (`tz`),
+ KEY `is_test` (`is_test`),
+ KEY `latest_open` (`latest_open`),
+ KEY `latest_click` (`latest_click`),
+ KEY `created` (`created`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+CREATE TABLE `templates` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) NOT NULL DEFAULT '',
+ `description` text,
+ `editor_name` varchar(50) DEFAULT '',
+ `editor_data` longtext,
+ `html` longtext,
+ `text` longtext,
+ `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ KEY `name` (`name`(191))
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+CREATE TABLE `trigger` (
+ `list` int(11) unsigned NOT NULL,
+ `subscription` int(11) unsigned NOT NULL,
+ `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`list`,`subscription`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+CREATE TABLE `triggers` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) NOT NULL DEFAULT '',
+ `description` text,
+ `enabled` tinyint(4) unsigned NOT NULL DEFAULT '1',
+ `list` int(11) unsigned NOT NULL,
+ `source_campaign` int(11) unsigned DEFAULT NULL,
+ `rule` varchar(255) CHARACTER SET ascii NOT NULL DEFAULT 'column',
+ `column` varchar(255) CHARACTER SET ascii DEFAULT NULL,
+ `seconds` int(11) NOT NULL DEFAULT '0',
+ `dest_campaign` int(11) unsigned DEFAULT NULL,
+ `count` int(11) unsigned NOT NULL DEFAULT '0',
+ `last_check` timestamp NULL DEFAULT NULL,
+ `created` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ KEY `name` (`name`(191)),
+ KEY `source_campaign` (`source_campaign`),
+ KEY `dest_campaign` (`dest_campaign`),
+ KEY `list` (`list`),
+ KEY `column` (`column`),
+ KEY `active` (`enabled`),
+ KEY `last_check` (`last_check`),
+ CONSTRAINT `triggers_ibfk_1` FOREIGN KEY (`list`) REFERENCES `lists` (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+CREATE TABLE `tzoffset` (
+ `tz` varchar(100) NOT NULL DEFAULT '',
+ `offset` int(11) NOT NULL DEFAULT '0',
+ PRIMARY KEY (`tz`)
+) ENGINE=InnoDB DEFAULT CHARSET=ascii;
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/abidjan',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/accra',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/addis_ababa',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/algiers',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/asmara',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/asmera',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/bamako',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/bangui',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/banjul',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/bissau',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/blantyre',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/brazzaville',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/bujumbura',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/cairo',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/casablanca',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/ceuta',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/conakry',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/dakar',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/dar_es_salaam',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/djibouti',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/douala',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/el_aaiun',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/freetown',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/gaborone',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/harare',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/johannesburg',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/juba',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/kampala',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/khartoum',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/kigali',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/kinshasa',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/lagos',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/libreville',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/lome',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/luanda',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/lubumbashi',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/lusaka',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/malabo',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/maputo',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/maseru',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/mbabane',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/mogadishu',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/monrovia',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/nairobi',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/ndjamena',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/niamey',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/nouakchott',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/ouagadougou',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/porto-novo',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/sao_tome',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/timbuktu',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/tripoli',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/tunis',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('africa/windhoek',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/adak',-540);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/anchorage',-480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/anguilla',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/antigua',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/araguaina',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/argentina/buenos_aires',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/argentina/catamarca',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/argentina/comodrivadavia',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/argentina/cordoba',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/argentina/jujuy',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/argentina/la_rioja',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/argentina/mendoza',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/argentina/rio_gallegos',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/argentina/salta',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/argentina/san_juan',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/argentina/san_luis',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/argentina/tucuman',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/argentina/ushuaia',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/aruba',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/asuncion',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/atikokan',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/atka',-540);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/bahia',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/bahia_banderas',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/barbados',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/belem',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/belize',-360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/blanc-sablon',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/boa_vista',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/bogota',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/boise',-360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/buenos_aires',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/cambridge_bay',-360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/campo_grande',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/cancun',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/caracas',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/catamarca',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/cayenne',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/cayman',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/chicago',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/chihuahua',-360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/coral_harbour',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/cordoba',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/costa_rica',-360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/creston',-420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/cuiaba',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/curacao',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/danmarkshavn',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/dawson',-420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/dawson_creek',-420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/denver',-360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/detroit',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/dominica',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/edmonton',-360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/eirunepe',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/el_salvador',-360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/ensenada',-420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/fortaleza',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/fort_nelson',-420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/fort_wayne',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/glace_bay',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/godthab',-120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/goose_bay',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/grand_turk',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/grenada',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/guadeloupe',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/guatemala',-360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/guayaquil',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/guyana',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/halifax',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/havana',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/hermosillo',-420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/indiana/indianapolis',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/indiana/knox',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/indiana/marengo',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/indiana/petersburg',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/indiana/tell_city',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/indiana/vevay',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/indiana/vincennes',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/indiana/winamac',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/indianapolis',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/inuvik',-360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/iqaluit',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/jamaica',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/jujuy',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/juneau',-480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/kentucky/louisville',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/kentucky/monticello',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/knox_in',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/kralendijk',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/la_paz',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/lima',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/los_angeles',-420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/louisville',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/lower_princes',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/maceio',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/managua',-360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/manaus',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/marigot',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/martinique',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/matamoros',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/mazatlan',-360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/mendoza',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/menominee',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/merida',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/metlakatla',-480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/mexico_city',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/miquelon',-120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/moncton',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/monterrey',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/montevideo',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/montreal',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/montserrat',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/nassau',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/new_york',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/nipigon',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/nome',-480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/noronha',-120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/north_dakota/beulah',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/north_dakota/center',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/north_dakota/new_salem',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/ojinaga',-360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/panama',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/pangnirtung',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/paramaribo',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/phoenix',-420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/port-au-prince',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/porto_acre',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/porto_velho',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/port_of_spain',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/puerto_rico',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/punta_arenas',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/rainy_river',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/rankin_inlet',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/recife',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/regina',-360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/resolute',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/rio_branco',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/rosario',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/santarem',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/santa_isabel',-420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/santiago',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/santo_domingo',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/sao_paulo',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/scoresbysund',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/shiprock',-360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/sitka',-480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/st_barthelemy',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/st_johns',-150);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/st_kitts',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/st_lucia',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/st_thomas',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/st_vincent',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/swift_current',-360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/tegucigalpa',-360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/thule',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/thunder_bay',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/tijuana',-420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/toronto',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/tortola',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/vancouver',-420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/virgin',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/whitehorse',-420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/winnipeg',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/yakutat',-480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('america/yellowknife',-360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('antarctica/casey',660);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('antarctica/davis',420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('antarctica/dumontdurville',600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('antarctica/macquarie',660);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('antarctica/mawson',300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('antarctica/mcmurdo',720);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('antarctica/palmer',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('antarctica/rothera',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('antarctica/south_pole',720);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('antarctica/syowa',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('antarctica/troll',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('antarctica/vostok',360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('arctic/longyearbyen',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/aden',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/almaty',360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/amman',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/anadyr',720);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/aqtau',300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/aqtobe',300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/ashgabat',300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/ashkhabad',300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/atyrau',300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/baghdad',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/bahrain',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/baku',240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/bangkok',420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/barnaul',420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/beirut',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/bishkek',360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/brunei',480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/calcutta',330);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/chita',540);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/choibalsan',480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/chongqing',480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/chungking',480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/colombo',330);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/dacca',360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/damascus',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/dhaka',360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/dili',540);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/dubai',240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/dushanbe',300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/famagusta',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/gaza',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/harbin',480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/hebron',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/hong_kong',480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/hovd',420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/ho_chi_minh',420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/irkutsk',480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/istanbul',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/jakarta',420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/jayapura',540);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/jerusalem',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/kabul',270);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/kamchatka',720);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/karachi',300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/kashgar',360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/kathmandu',345);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/katmandu',345);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/khandyga',540);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/kolkata',330);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/krasnoyarsk',420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/kuala_lumpur',480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/kuching',480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/kuwait',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/macao',480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/macau',480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/magadan',660);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/makassar',480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/manila',480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/muscat',240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/nicosia',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/novokuznetsk',420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/novosibirsk',420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/omsk',360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/oral',300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/phnom_penh',420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/pontianak',420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/pyongyang',510);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/qatar',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/qyzylorda',360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/rangoon',390);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/riyadh',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/saigon',420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/sakhalin',660);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/samarkand',300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/seoul',540);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/shanghai',480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/singapore',480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/srednekolymsk',660);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/taipei',480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/tashkent',300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/tbilisi',240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/tehran',270);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/tel_aviv',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/thimbu',360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/thimphu',360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/tokyo',540);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/tomsk',420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/ujung_pandang',480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/ulaanbaatar',480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/ulan_bator',480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/urumqi',360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/ust-nera',600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/vientiane',420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/vladivostok',600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/yakutsk',540);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/yangon',390);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/yekaterinburg',300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('asia/yerevan',240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('atlantic/azores',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('atlantic/bermuda',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('atlantic/canary',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('atlantic/cape_verde',-60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('atlantic/faeroe',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('atlantic/faroe',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('atlantic/jan_mayen',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('atlantic/madeira',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('atlantic/reykjavik',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('atlantic/south_georgia',-120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('atlantic/stanley',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('atlantic/st_helena',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/act',600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/adelaide',570);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/brisbane',600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/broken_hill',570);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/canberra',600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/currie',600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/darwin',570);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/eucla',525);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/hobart',600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/lhi',630);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/lindeman',600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/lord_howe',630);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/melbourne',600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/north',570);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/nsw',600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/perth',480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/queensland',600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/south',570);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/sydney',600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/tasmania',600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/victoria',600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/west',480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('australia/yancowinna',570);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('brazil/acre',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('brazil/denoronha',-120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('brazil/east',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('brazil/west',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/atlantic',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/central',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/east-saskatchewan',-360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/eastern',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/mountain',-360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/newfoundland',-150);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/pacific',-420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/saskatchewan',-360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('canada/yukon',-420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('cet',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('chile/continental',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('chile/easterisland',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('cst6cdt',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('cuba',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('eet',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('egypt',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('eire',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('est',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('est5edt',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt+0',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt+1',-60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt+10',-600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt+11',-660);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt+12',-720);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt+2',-120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt+3',-180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt+4',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt+5',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt+6',-360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt+7',-420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt+8',-480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt+9',-540);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-0',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-1',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-10',600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-11',660);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-12',720);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-13',780);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-14',840);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-2',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-3',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-4',240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-5',300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-6',360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-7',420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-8',480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt-9',540);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/gmt0',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/greenwich',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/uct',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/universal',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/utc',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('etc/zulu',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/amsterdam',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/andorra',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/astrakhan',240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/athens',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/belfast',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/belgrade',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/berlin',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/bratislava',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/brussels',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/bucharest',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/budapest',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/busingen',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/chisinau',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/copenhagen',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/dublin',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/gibraltar',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/guernsey',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/helsinki',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/isle_of_man',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/istanbul',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/jersey',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/kaliningrad',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/kiev',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/kirov',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/lisbon',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/ljubljana',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/london',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/luxembourg',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/madrid',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/malta',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/mariehamn',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/minsk',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/monaco',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/moscow',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/nicosia',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/oslo',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/paris',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/podgorica',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/prague',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/riga',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/rome',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/samara',240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/san_marino',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/sarajevo',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/saratov',240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/simferopol',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/skopje',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/sofia',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/stockholm',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/tallinn',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/tirane',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/tiraspol',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/ulyanovsk',240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/uzhgorod',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/vaduz',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/vatican',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/vienna',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/vilnius',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/volgograd',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/warsaw',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/zagreb',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/zaporozhye',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('europe/zurich',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('gb',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('gb-eire',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('gmt',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('gmt+0',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('gmt-0',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('gmt0',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('greenwich',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('hongkong',480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('hst',-600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('iceland',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('indian/antananarivo',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('indian/chagos',360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('indian/christmas',420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('indian/cocos',390);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('indian/comoro',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('indian/kerguelen',300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('indian/mahe',240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('indian/maldives',300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('indian/mauritius',240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('indian/mayotte',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('indian/reunion',240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('iran',270);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('israel',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('jamaica',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('japan',540);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('kwajalein',720);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('libya',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('met',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('mexico/bajanorte',-420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('mexico/bajasur',-360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('mexico/general',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('mst',-420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('mst7mdt',-360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('navajo',-360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('nz',720);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('nz-chat',765);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/apia',780);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/auckland',720);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/bougainville',660);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/chatham',765);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/chuuk',600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/easter',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/efate',660);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/enderbury',780);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/fakaofo',780);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/fiji',720);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/funafuti',720);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/galapagos',-360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/gambier',-540);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/guadalcanal',660);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/guam',600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/honolulu',-600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/johnston',-600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/kiritimati',840);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/kosrae',660);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/kwajalein',720);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/majuro',720);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/marquesas',-570);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/midway',-660);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/nauru',720);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/niue',-660);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/norfolk',660);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/noumea',660);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/pago_pago',-660);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/palau',540);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/pitcairn',-480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/pohnpei',660);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/ponape',660);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/port_moresby',600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/rarotonga',-600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/saipan',600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/samoa',-660);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/tahiti',-600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/tarawa',720);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/tongatapu',780);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/truk',600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/wake',720);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/wallis',720);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pacific/yap',600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('poland',120);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('portugal',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('prc',480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('pst8pdt',-420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('roc',480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('rok',540);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('singapore',480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('turkey',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('uct',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('universal',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('us/alaska',-480);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('us/aleutian',-540);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('us/arizona',-420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('us/central',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('us/east-indiana',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('us/eastern',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('us/hawaii',-600);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('us/indiana-starke',-300);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('us/michigan',-240);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('us/mountain',-360);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('us/pacific',-420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('us/pacific-new',-420);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('us/samoa',-660);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('utc',0);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('w-su',180);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('wet',60);
+INSERT INTO `tzoffset` (`tz`, `offset`) VALUES ('zulu',0);
+CREATE TABLE `users` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `username` varchar(255) NOT NULL DEFAULT '',
+ `password` varchar(255) NOT NULL DEFAULT '',
+ `email` varchar(255) CHARACTER SET utf8 DEFAULT NULL,
+ `access_token` varchar(40) DEFAULT NULL,
+ `reset_token` varchar(255) CHARACTER SET ascii DEFAULT NULL,
+ `reset_expire` timestamp NULL DEFAULT NULL,
+ `created` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `email` (`email`),
+ KEY `username` (`username`(191)),
+ KEY `reset` (`reset_token`),
+ KEY `check_reset` (`username`(191),`reset_token`,`reset_expire`),
+ KEY `token_index` (`access_token`)
+) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
+INSERT INTO `users` (`id`, `username`, `password`, `email`, `access_token`, `reset_token`, `reset_expire`, `created`) VALUES (1,'admin','$2a$10$mzKU71G62evnGB2PvQA4k..Wf9jASk.c7a8zRMHh6qQVjYJ2r/g/K','admin@example.com',NULL,NULL,NULL,NOW());
+
+SET UNIQUE_CHECKS=1;
+SET FOREIGN_KEY_CHECKS=1;
diff --git a/test/e2e/.eslintrc b/test/e2e/.eslintrc
new file mode 100644
index 00000000..836bac9a
--- /dev/null
+++ b/test/e2e/.eslintrc
@@ -0,0 +1,11 @@
+{
+ "parser": "babel-eslint",
+ "rules": {
+ "strict": 0,
+ "no-invalid-this": 0,
+ "no-unused-expressions": 0
+ },
+ "env": {
+ "mocha": true
+ }
+}
diff --git a/test/e2e/README.md b/test/e2e/README.md
new file mode 100644
index 00000000..f1147d08
--- /dev/null
+++ b/test/e2e/README.md
@@ -0,0 +1,6 @@
+Running e2e tests requires Node 7.6 or later and a dedicated test database.
+
+1. Start Mailtrain with `npm run startest`
+2. Start e2e tests with `npm run e2e`
+
+By default the tests run with `phantomjs`. To use different browsers see `test/e2e/bin/README.md`.
diff --git a/test/e2e/bin/README.md b/test/e2e/bin/README.md
new file mode 100644
index 00000000..5edbe684
--- /dev/null
+++ b/test/e2e/bin/README.md
@@ -0,0 +1,8 @@
+This directory serves for custom browser drivers.
+
+1. https://seleniumhq.github.io/selenium/docs/api/javascript/
+2. Download a driver of your choice and put it into this directory
+3. chmod +x driver
+4. Edit config/test.toml
+
+Current Firefox issue (and patch): https://github.com/mozilla/geckodriver/issues/683
diff --git a/test/e2e/helpers/config.js b/test/e2e/helpers/config.js
new file mode 100644
index 00000000..71726ff7
--- /dev/null
+++ b/test/e2e/helpers/config.js
@@ -0,0 +1,31 @@
+'use strict';
+
+const config = require('config');
+
+module.exports = {
+ app: config,
+ baseUrl: 'http://localhost:' + config.www.port,
+ users: {
+ admin: {
+ username: 'admin',
+ password: 'test'
+ }
+ },
+ lists: {
+ one: {
+ id: 1,
+ cid: 'Hkj1vCoJb',
+ publicSubscribe: 1,
+ unsubscriptionMode: 0
+ }
+ },
+ settings: {
+ 'service-url' : 'http://localhost:' + config.www.port + '/',
+ 'default-homepage': 'https://mailtrain.org',
+ 'smtp-hostname': config.testserver.host,
+ 'smtp-port': config.testserver.port,
+ 'smtp-encryption': 'NONE',
+ 'smtp-user': config.testserver.username,
+ 'smtp-pass': config.testserver.password
+ }
+};
diff --git a/test/e2e/helpers/driver.js b/test/e2e/helpers/driver.js
new file mode 100644
index 00000000..a9b8444b
--- /dev/null
+++ b/test/e2e/helpers/driver.js
@@ -0,0 +1,15 @@
+'use strict';
+
+const config = require('./config');
+const webdriver = require('selenium-webdriver');
+
+const driver = new webdriver.Builder()
+ .forBrowser(config.app.seleniumwebdriver.browser || 'phantomjs')
+ .build();
+
+if (global.USE_SHARED_DRIVER === true) {
+ driver.originalQuit = driver.quit;
+ driver.quit = () => {};
+}
+
+module.exports = driver;
diff --git a/test/e2e/helpers/exit-unless-test.js b/test/e2e/helpers/exit-unless-test.js
new file mode 100644
index 00000000..e53b2eed
--- /dev/null
+++ b/test/e2e/helpers/exit-unless-test.js
@@ -0,0 +1,16 @@
+'use strict';
+
+const config = require('./config');
+const log = require('npmlog');
+const path = require('path');
+const fs = require('fs');
+
+if (process.env.NODE_ENV !== 'test' || !fs.existsSync(path.join(__dirname, '..', '..', '..', 'config', 'test.toml'))) {
+ log.error('e2e', 'This script only runs in test and config/test.toml (i.e. a dedicated test database) is present');
+ process.exit(1);
+}
+
+if (config.app.testserver.enabled !== true) {
+ log.error('e2e', 'This script only runs if the testserver is enabled. Check config/test.toml');
+ process.exit(1);
+}
diff --git a/test/e2e/index.js b/test/e2e/index.js
new file mode 100644
index 00000000..d78f5819
--- /dev/null
+++ b/test/e2e/index.js
@@ -0,0 +1,36 @@
+'use strict';
+
+require('./helpers/exit-unless-test');
+
+global.USE_SHARED_DRIVER = true;
+
+const driver = require('./helpers/driver');
+const only = 'only';
+const skip = 'skip';
+
+
+
+let tests = [
+ ['tests/login'],
+ ['tests/subscription']
+];
+
+
+
+tests = tests.filter(t => t[1] !== skip);
+
+if (tests.some(t => t[1] === only)) {
+ tests = tests.filter(t => t[1] === only);
+}
+
+describe('e2e', function() {
+ this.timeout(10000);
+
+ tests.forEach(t => {
+ describe(t[0], () => {
+ require('./' + t[0]); // eslint-disable-line global-require
+ });
+ });
+
+ after(() => driver.originalQuit());
+});
diff --git a/test/e2e/page-objects/flash.js b/test/e2e/page-objects/flash.js
new file mode 100644
index 00000000..2fd7d8da
--- /dev/null
+++ b/test/e2e/page-objects/flash.js
@@ -0,0 +1,25 @@
+'use strict';
+
+const Page = require('./page');
+let flash;
+
+class Flash extends Page {
+ getText() {
+ return this.element('alert').getText();
+ }
+ clear() {
+ return this.driver.executeScript(`
+ var elements = document.getElementsByClassName('alert');
+ while(elements.length > 0){
+ elements[0].parentNode.removeChild(elements[0]);
+ }
+ `);
+ }
+}
+
+module.exports = driver => flash || new Flash(driver, {
+ elementToWaitFor: 'alert',
+ elements: {
+ alert: 'div.alert:not(.js-warning)'
+ }
+});
diff --git a/test/e2e/page-objects/home.js b/test/e2e/page-objects/home.js
new file mode 100644
index 00000000..72ad84a7
--- /dev/null
+++ b/test/e2e/page-objects/home.js
@@ -0,0 +1,12 @@
+'use strict';
+
+const Page = require('./page');
+let home;
+
+module.exports = driver => home || new Page(driver, {
+ url: '/',
+ elementToWaitFor: 'body',
+ elements: {
+ body: 'body.page--home'
+ }
+});
diff --git a/test/e2e/page-objects/page.js b/test/e2e/page-objects/page.js
new file mode 100644
index 00000000..16c2a6cf
--- /dev/null
+++ b/test/e2e/page-objects/page.js
@@ -0,0 +1,61 @@
+'use strict';
+
+const config = require('../helpers/config');
+const webdriver = require('selenium-webdriver');
+const By = webdriver.By;
+const until = webdriver.until;
+
+class Page {
+ constructor(driver, props) {
+ this.driver = driver;
+ this.props = props || {
+ elements: {}
+ };
+ }
+
+ element(key) {
+ return this.driver.findElement(By.css(this.props.elements[key] || key));
+ }
+
+ navigate() {
+ this.driver.navigate().to(config.baseUrl + this.props.url);
+ return this.waitUntilVisible();
+ }
+
+ waitUntilVisible() {
+ let selector = this.props.elements[this.props.elementToWaitFor];
+ if (!selector && this.props.url) {
+ selector = 'body.page--' + (this.props.url.substring(1).replace(/\//g, '--') || 'home');
+ }
+ return selector ? this.driver.wait(until.elementLocated(By.css(selector))) : this.driver.sleep(1000);
+ }
+
+ submit() {
+ return this.element('submitButton').click();
+ }
+
+ click(key) {
+ return this.element(key).click();
+ }
+
+ getText(key) {
+ return this.element(key).getText();
+ }
+
+ getValue(key) {
+ return this.element(key).getAttribute('value');
+ }
+
+ setValue(key, value) {
+ return this.element(key).sendKeys(value);
+ }
+
+ containsText(str) {
+ // let text = await driver.findElement({ css: 'body' }).getText();
+ return this.driver.executeScript(`
+ return (document.documentElement.textContent || document.documentElement.innerText).indexOf('${str}') > -1;
+ `);
+ }
+}
+
+module.exports = Page;
diff --git a/test/e2e/page-objects/subscription.js b/test/e2e/page-objects/subscription.js
new file mode 100644
index 00000000..858dfdf7
--- /dev/null
+++ b/test/e2e/page-objects/subscription.js
@@ -0,0 +1,84 @@
+'use strict';
+
+const config = require('../helpers/config');
+const Page = require('./page');
+
+class Web extends Page {
+ enterEmail(value) {
+ this.element('emailInput').clear();
+ return this.element('emailInput').sendKeys(value);
+ }
+}
+
+class Mail extends Page {
+ navigate(address) {
+ this.driver.sleep(100);
+ this.driver.navigate().to(`http://localhost:${config.app.testserver.mailboxserverport}/${address}`);
+ return this.waitUntilVisible();
+ }
+}
+
+module.exports = (driver, list) => ({
+
+ webSubscribe: new Web(driver, {
+ url: `/subscription/${list.cid}`,
+ elementToWaitFor: 'form',
+ elements: {
+ form: `form[action="/subscription/${list.cid}/subscribe"]`,
+ emailInput: '#main-form input[name="email"]',
+ submitButton: 'a[href="#submit"]'
+ }
+ }),
+
+ webConfirmSubscriptionNotice: new Web(driver, {
+ url: `/subscription/${list.cid}/confirm-notice`,
+ elementToWaitFor: 'homepageButton',
+ elements: {
+ homepageButton: `a[href="${config.settings['default-homepage']}"]`
+ }
+ }),
+
+ mailConfirmSubscription: new Mail(driver, {
+ elementToWaitFor: 'confirmLink',
+ elements: {
+ confirmLink: `a[href^="${config.settings['service-url']}subscription/subscribe/"]`
+ }
+ }),
+
+ webSubscribedNotice: new Web(driver, {
+ elementToWaitFor: 'homepageButton',
+ elements: {
+ homepageButton: 'a[href^="https://mailtrain.org"]'
+ }
+ }),
+
+ mailSubscriptionConfirmed: new Mail(driver, {
+ elementToWaitFor: 'unsubscribeLink',
+ elements: {
+ unsubscribeLink: 'a[href*="/unsubscribe/"]',
+ manageLink: 'a[href*="/manage/"]'
+ }
+ }),
+
+ webUnsubscribe: new Web(driver, {
+ elementToWaitFor: 'submitButton',
+ elements: {
+ submitButton: 'a[href="#submit"]'
+ }
+ }),
+
+ webUnsubscribedNotice: new Web(driver, {
+ elementToWaitFor: 'homepageButton',
+ elements: {
+ homepageButton: 'a[href^="https://mailtrain.org"]'
+ }
+ }),
+
+ mailUnsubscriptionConfirmed: new Mail(driver, {
+ elementToWaitFor: 'resubscribeLink',
+ elements: {
+ resubscribeLink: `a[href^="${config.settings['service-url']}subscription/${list.cid}"]`
+ }
+ })
+
+});
diff --git a/test/e2e/page-objects/users.js b/test/e2e/page-objects/users.js
new file mode 100644
index 00000000..b2c17b58
--- /dev/null
+++ b/test/e2e/page-objects/users.js
@@ -0,0 +1,35 @@
+'use strict';
+
+const Page = require('./page');
+
+class Login extends Page {
+ enterUsername(value) {
+ // this.element('usernameInput').clear();
+ return this.element('usernameInput').sendKeys(value);
+ }
+ enterPassword(value) {
+ return this.element('passwordInput').sendKeys(value);
+ }
+}
+
+module.exports = driver => ({
+
+ login: new Login(driver, {
+ url: '/users/login',
+ elementToWaitFor: 'submitButton',
+ elements: {
+ usernameInput: 'form[action="/users/login"] input[name="username"]',
+ passwordInput: 'form[action="/users/login"] input[name="password"]',
+ submitButton: 'form[action="/users/login"] [type=submit]'
+ }
+ }),
+
+ account: new Page(driver, {
+ url: '/users/account',
+ elementToWaitFor: 'emailInput',
+ elements: {
+ emailInput: 'form[action="/users/account"] input[name="email"]'
+ }
+ })
+
+});
diff --git a/test/e2e/tests/login.js b/test/e2e/tests/login.js
new file mode 100644
index 00000000..238b5def
--- /dev/null
+++ b/test/e2e/tests/login.js
@@ -0,0 +1,57 @@
+'use strict';
+
+const config = require('../helpers/config');
+const expect = require('chai').expect;
+const driver = require('../helpers/driver');
+const home = require('../page-objects/home')(driver);
+const flash = require('../page-objects/flash')(driver);
+const {
+ login,
+ account
+} = require('../page-objects/users')(driver);
+
+describe('login', function() {
+ this.timeout(10000);
+
+ before(() => driver.manage().deleteAllCookies());
+
+ it('can access home page', async () => {
+ await home.navigate();
+ });
+
+ it('can not access restricted content', async () => {
+ driver.navigate().to(config.baseUrl + '/settings');
+ flash.waitUntilVisible();
+ expect(await flash.getText()).to.contain('Need to be logged in to access restricted content');
+ await flash.clear();
+ });
+
+ it('can not login with false credentials', async () => {
+ login.enterUsername(config.users.admin.username);
+ login.enterPassword('invalid');
+ login.submit();
+ flash.waitUntilVisible();
+ expect(await flash.getText()).to.contain('Incorrect username or password');
+ await flash.clear();
+ });
+
+ it('can login as admin', async () => {
+ login.enterUsername(config.users.admin.username);
+ login.enterPassword(config.users.admin.password);
+ login.submit();
+ flash.waitUntilVisible();
+ expect(await flash.getText()).to.contain('Logged in as admin');
+ });
+
+ it('can access account page as admin', async () => {
+ await account.navigate();
+ });
+
+ it('can logout', async () => {
+ driver.navigate().to(config.baseUrl + '/users/logout');
+ flash.waitUntilVisible();
+ expect(await flash.getText()).to.contain('logged out');
+ });
+
+ after(() => driver.quit());
+});
diff --git a/test/e2e/tests/subscription.js b/test/e2e/tests/subscription.js
new file mode 100644
index 00000000..3360c738
--- /dev/null
+++ b/test/e2e/tests/subscription.js
@@ -0,0 +1,101 @@
+'use strict';
+
+const config = require('../helpers/config');
+const shortid = require('shortid');
+const expect = require('chai').expect;
+const driver = require('../helpers/driver');
+const Page = require('../page-objects/page');
+
+const page = new Page(driver);
+const flash = require('../page-objects/flash')(driver);
+const {
+ webSubscribe,
+ webConfirmSubscriptionNotice,
+ mailConfirmSubscription,
+ webSubscribedNotice,
+ mailSubscriptionConfirmed,
+ webUnsubscribe,
+ webUnsubscribedNotice,
+ mailUnsubscriptionConfirmed
+} = require('../page-objects/subscription')(driver, config.lists.one);
+
+const testuser = {
+ email: 'keep.' + shortid.generate() + '@mailtrain.org'
+};
+
+// console.log(testuser.email);
+
+describe('subscribe (list one)', function() {
+ this.timeout(10000);
+
+ before(() => driver.manage().deleteAllCookies());
+
+ it('visits web-subscribe', async () => {
+ await webSubscribe.navigate();
+ });
+
+ it('submits invalid email (error)', async () => {
+ webSubscribe.enterEmail('foo@bar.nope');
+ webSubscribe.submit();
+ flash.waitUntilVisible();
+ expect(await flash.getText()).to.contain('Invalid email address');
+ });
+
+ it('submits valid email', async () => {
+ webSubscribe.enterEmail(testuser.email);
+ await webSubscribe.submit();
+ });
+
+ it('sees web-confirm-subscription-notice', async () => {
+ webConfirmSubscriptionNotice.waitUntilVisible();
+ expect(await page.containsText('Almost Finished')).to.be.true;
+ });
+
+ it('receives mail-confirm-subscription', async () => {
+ mailConfirmSubscription.navigate(testuser.email);
+ expect(await page.containsText('Please Confirm Subscription')).to.be.true;
+ });
+
+ it('clicks confirm subscription', async () => {
+ await mailConfirmSubscription.click('confirmLink');
+ });
+
+ it('sees web-subscribed-notice', async () => {
+ webSubscribedNotice.waitUntilVisible();
+ expect(await page.containsText('Subscription Confirmed')).to.be.true;
+ });
+
+ it('receives mail-subscription-confirmed', async () => {
+ mailSubscriptionConfirmed.navigate(testuser.email);
+ expect(await page.containsText('Subscription Confirmed')).to.be.true;
+ });
+});
+
+describe('unsubscribe (list one)', function() {
+ this.timeout(10000);
+
+ it('clicks unsubscribe', async () => {
+ await mailSubscriptionConfirmed.click('unsubscribeLink');
+ });
+
+ it('sees web-unsubscribe', async () => {
+ webUnsubscribe.waitUntilVisible();
+ expect(await page.containsText('Unsubscribe')).to.be.true;
+ });
+
+ it('clicks confirm unsubscription', async () => {
+ await webUnsubscribe.submit();
+ });
+
+ it('sees web-unsubscribed-notice', async () => {
+ webUnsubscribedNotice.waitUntilVisible();
+ expect(await page.containsText('Unsubscribe Successful')).to.be.true;
+ });
+
+ it('receives mail-unsubscription-confirmed', async () => {
+ mailUnsubscriptionConfirmed.navigate(testuser.email);
+ expect(await page.containsText('You Are Now Unsubscribed')).to.be.true;
+ });
+
+ after(() => driver.quit());
+});
diff --git a/test/frontmail-test.js b/test/nodeunit/frontmail-test.js
similarity index 100%
rename from test/frontmail-test.js
rename to test/nodeunit/frontmail-test.js
diff --git a/views/layout.hbs b/views/layout.hbs
index 339e8006..73eff28a 100644
--- a/views/layout.hbs
+++ b/views/layout.hbs
@@ -35,7 +35,7 @@
-
+