This commit is contained in:
Synox 2016-07-02 23:05:44 +02:00
commit d4dcbc5a67
64 changed files with 3030 additions and 0 deletions

3
.babelrc Normal file
View file

@ -0,0 +1,3 @@
{
"presets": ["es2015"]
}

5
.gitignore vendored Normal file
View file

@ -0,0 +1,5 @@
logs
node_modules/
.vagrant
.idea
target

52
.jshintrc Normal file
View file

@ -0,0 +1,52 @@
{
"esnext": true,
"camelcase": true,
"devel": false,
"asi": false,
"boss": false,
"eqnull": false,
"es5": true,
"moz": false,
"evil": false,
"expr": true,
"funcscope": true,
"iterator": false,
"lastsemic": false,
"laxbreak": false,
"laxcomma": false,
"loopfunc": false,
"multistr": true,
"noyield": false,
"notypeof": false,
"proto": false,
"scripturl": false,
"shadow": false,
"sub": false,
"supernew": false,
"validthis": false,
"plusplus": false,
"jquery": true,
"mocha": false,
"node": true,
"latedef": true,
"quotmark": "single",
"maxparams": 6,
"maxdepth": 5,
"maxerr": 50,
"globalstrict": false,
"globals": {
"inject": true,
"angular": true,
"require": true,
"process": true,
"module": true,
"describe": true,
"beforeEach": true,
"afterEach": true,
"it": true,
"expect": true,
"__dirname": true,
"exports": true,
"spyOn": true
}
}

53
Vagrantfile vendored Normal file
View file

@ -0,0 +1,53 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
VAGRANTFILE_API_VERSION = "2"
Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
config.vm.box = "avenuefactory/lamp"
config.vm.network "forwarded_port", guest: 80, host: 8080
config.vm.network "forwarded_port", guest: 993, host: 9993
config.vm.synced_folder "./", "/var/www/html", id: "vagrant-root",
owner: "vagrant",
group: "www-data",
mount_options: ["dmode=775,fmode=664"]
config.vm.provision "shell", inline: <<-SHELL
echo updating... && sudo apt-get -qq update
echo installing... && sudo apt-get -qq -y install php5-imap
sudo service apache2 restart
# Install and Configure Dovecot (http://blog.tedivm.com/open-source/2014/03/building-an-email-library-testing-with-vagrant-dovecot-and-travis-ci/)
#https://github.com/tedious/DovecotTesting/blob/master/resources/Scripts/Provision.sh
if which dovecot > /dev/null; then
echo 'Dovecot is already installed'
else
sudo mkdir /home/vagrant/Maildir
sudo chown -R vagrant:vagrant /home/vagrant/Maildir
sudo chmod a+rw /home/vagrant/Maildir
echo 'Installing Dovecot'
sudo apt-get -qq -y install dovecot-imapd
sudo touch /etc/dovecot/local.conf
sudo chmod go+rw /etc/dovecot/local.conf
echo 'mail_location = maildir:/home/vagrant/Maildir' >> /etc/dovecot/local.conf
echo 'disable_plaintext_auth = no' >> /etc/dovecot/local.conf
echo 'mail_max_userip_connections = 10000' >> /etc/dovecot/local.conf
sudo restart dovecot
fi
# Create user "test"
if getent passwd test > /dev/null; then
echo 'test already exists'
else
sudo useradd "test" -m -s /bin/bash
echo "test:test"|sudo chpasswd
echo 'User "test" created'
fi
SHELL
end

11
build.sh Executable file
View file

@ -0,0 +1,11 @@
#!/bin/bash -e
# install php dependencies
composer install
# copy backend
cp -rv src/{backend,config.sample}.php dist/
# build Javascript frontend
npm install
gulp build

8
composer.json Normal file
View file

@ -0,0 +1,8 @@
{
"require": {
"php-imap/php-imap": "~2.0"
},
"config": {
"vendor-dir": "dist/backend-libs"
}
}

62
composer.lock generated Normal file
View file

@ -0,0 +1,62 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"This file is @generated automatically"
],
"hash": "d4d6c6077bbcd7958a06026e4117a70b",
"content-hash": "357059f60b3f57be6e5f15b1df393a72",
"packages": [
{
"name": "php-imap/php-imap",
"version": "2.0.3",
"source": {
"type": "git",
"url": "https://github.com/barbushin/php-imap.git",
"reference": "cc1a49a3f68090db182183c59ffbc09055d59f5b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barbushin/php-imap/zipball/cc1a49a3f68090db182183c59ffbc09055d59f5b",
"reference": "cc1a49a3f68090db182183c59ffbc09055d59f5b",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"type": "library",
"autoload": {
"psr-0": {
"PhpImap": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD 3-Clause"
],
"authors": [
{
"name": "Sergey Barbushin",
"email": "barbushin@gmail.com",
"homepage": "http://linkedin.com/in/barbushin"
}
],
"description": "PHP class to access mailbox by POP3/IMAP/NNTP using IMAP extension",
"homepage": "https://github.com/barbushin/php-imap",
"keywords": [
"imap",
"mail",
"php"
],
"time": "2015-09-16 07:40:39"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": [],
"platform-dev": []
}

7
dist/backend-libs/autoload.php vendored Normal file
View file

@ -0,0 +1,7 @@
<?php
// autoload.php @generated by Composer
require_once __DIR__ . '/composer' . '/autoload_real.php';
return ComposerAutoloaderInit38e9d02473bfdb3672d6b8a39de2fbc0::getLoader();

View file

@ -0,0 +1,413 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see http://www.php-fig.org/psr/psr-0/
* @see http://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
// PSR-4
private $prefixLengthsPsr4 = array();
private $prefixDirsPsr4 = array();
private $fallbackDirsPsr4 = array();
// PSR-0
private $prefixesPsr0 = array();
private $fallbackDirsPsr0 = array();
private $useIncludePath = false;
private $classMap = array();
private $classMapAuthoritative = false;
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', $this->prefixesPsr0);
}
return array();
}
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array $classMap Class to filename map
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*/
public function add($prefix, $paths, $prepend = false)
{
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
(array) $paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
(array) $paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = (array) $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
(array) $paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
(array) $paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
(array) $paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
(array) $paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
(array) $paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param array|string $paths The PSR-0 base directories
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param array|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
}
/**
* Unregisters this instance as an autoloader.
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return bool|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
includeFile($file);
return true;
}
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// work around for PHP 5.3.0 - 5.3.2 https://bugs.php.net/50731
if ('\\' == $class[0]) {
$class = substr($class, 1);
}
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative) {
return false;
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if ($file === null && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if ($file === null) {
// Remember that this class does not exist.
return $this->classMap[$class] = false;
}
return $file;
}
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
foreach ($this->prefixLengthsPsr4[$first] as $prefix => $length) {
if (0 === strpos($class, $prefix)) {
foreach ($this->prefixDirsPsr4[$prefix] as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $length))) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
}
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*/
function includeFile($file)
{
include $file;
}

21
dist/backend-libs/composer/LICENSE vendored Normal file
View file

@ -0,0 +1,21 @@
Copyright (c) 2016 Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View file

@ -0,0 +1,9 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname(dirname($vendorDir));
return array(
);

View file

@ -0,0 +1,10 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname(dirname($vendorDir));
return array(
'PhpImap' => array($vendorDir . '/php-imap/php-imap/src'),
);

View file

@ -0,0 +1,9 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname(dirname($vendorDir));
return array(
);

View file

@ -0,0 +1,52 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit38e9d02473bfdb3672d6b8a39de2fbc0
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
spl_autoload_register(array('ComposerAutoloaderInit38e9d02473bfdb3672d6b8a39de2fbc0', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
spl_autoload_unregister(array('ComposerAutoloaderInit38e9d02473bfdb3672d6b8a39de2fbc0', 'loadClassLoader'));
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION');
if ($useStaticLoader) {
require_once __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit38e9d02473bfdb3672d6b8a39de2fbc0::getInitializer($loader));
} else {
$map = require __DIR__ . '/autoload_namespaces.php';
foreach ($map as $namespace => $path) {
$loader->set($namespace, $path);
}
$map = require __DIR__ . '/autoload_psr4.php';
foreach ($map as $namespace => $path) {
$loader->setPsr4($namespace, $path);
}
$classMap = require __DIR__ . '/autoload_classmap.php';
if ($classMap) {
$loader->addClassMap($classMap);
}
}
$loader->register(true);
return $loader;
}
}

View file

@ -0,0 +1,26 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit38e9d02473bfdb3672d6b8a39de2fbc0
{
public static $prefixesPsr0 = array (
'P' =>
array (
'PhpImap' =>
array (
0 => __DIR__ . '/..' . '/php-imap/php-imap/src',
),
),
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixesPsr0 = ComposerStaticInit38e9d02473bfdb3672d6b8a39de2fbc0::$prefixesPsr0;
}, null, ClassLoader::class);
}
}

View file

@ -0,0 +1,47 @@
[
{
"name": "php-imap/php-imap",
"version": "2.0.3",
"version_normalized": "2.0.3.0",
"source": {
"type": "git",
"url": "https://github.com/barbushin/php-imap.git",
"reference": "cc1a49a3f68090db182183c59ffbc09055d59f5b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/barbushin/php-imap/zipball/cc1a49a3f68090db182183c59ffbc09055d59f5b",
"reference": "cc1a49a3f68090db182183c59ffbc09055d59f5b",
"shasum": ""
},
"require": {
"php": ">=5.3.0"
},
"time": "2015-09-16 07:40:39",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-0": {
"PhpImap": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD 3-Clause"
],
"authors": [
{
"name": "Sergey Barbushin",
"email": "barbushin@gmail.com",
"homepage": "http://linkedin.com/in/barbushin"
}
],
"description": "PHP class to access mailbox by POP3/IMAP/NNTP using IMAP extension",
"homepage": "https://github.com/barbushin/php-imap",
"keywords": [
"imap",
"mail",
"php"
]
}
]

View file

@ -0,0 +1,22 @@
# Auto detect text files and perform LF normalization
* text=auto
# Custom for Visual Studio
*.cs diff=csharp
*.sln merge=union
*.csproj merge=union
*.vbproj merge=union
*.fsproj merge=union
*.dbproj merge=union
# Standard to msysgit
*.doc diff=astextplain
*.DOC diff=astextplain
*.docx diff=astextplain
*.DOCX diff=astextplain
*.dot diff=astextplain
*.DOT diff=astextplain
*.pdf diff=astextplain
*.PDF diff=astextplain
*.rtf diff=astextplain
*.RTF diff=astextplain

View file

@ -0,0 +1,164 @@
#################
## Eclipse
#################
*.pydevproject
.project
.metadata
.idea
bin/
tmp/
*.tmp
*.bak
*.swp
*~.nib
local.properties
.classpath
.settings/
.loadpath
# External tool builders
.externalToolBuilders/
# Locally stored "Eclipse launch configurations"
*.launch
# CDT-specific
.cproject
# PDT-specific
.buildpath
#################
## Visual Studio
#################
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
# User-specific files
*.suo
*.user
*.sln.docstates
# Build results
[Dd]ebug/
[Rr]elease/
*_i.c
*_p.c
*.ilk
*.meta
*.obj
*.pch
*.pdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.vspscc
.builds
*.dotCover
## TODO: If you have NuGet Package Restore enabled, uncomment this
#packages/
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opensdf
*.sdf
# Visual Studio profiler
*.psess
*.vsp
# ReSharper is a .NET coding add-in
_ReSharper*
# Installshield output folder
[Ee]xpress
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish
# Others
[Bb]in
[Oo]bj
sql
TestResults
*.Cache
ClientBin
stylecop.*
~$*
*.dbmdl
Generated_Code #added for RIA/Silverlight projects
# Backup & report files from converting an old project file to a newer
# Visual Studio version. Backup files are not needed, because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
############
## Windows
############
# Windows image file caches
Thumbs.db
# Folder config file
Desktop.ini
#############
## Python
#############
*.py[co]
# Packages
*.egg
*.egg-info
dist
build
eggs
parts
bin
var
sdist
develop-eggs
.installed.cfg
# Installer logs
pip-log.txt
# Unit test / coverage reports
.coverage
.tox
#Translations
*.mo
#Mr Developer
.mr.developer.cfg
# Mac crap
.DS_Store

View file

@ -0,0 +1,32 @@
ImapMailbox
Copyright (c) 2012 by Barbushin Sergey <barbushin@gmail.com>.
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.
* The names of the contributors may not be used to endorse or
promote products derived from this software without specific
prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View file

@ -0,0 +1,53 @@
ImapMailbox is PHP class to access mailbox by POP3/IMAP/NNTP using IMAP extension
### Features
* Connect to mailbox by POP3/IMAP/NNTP (see [imap_open](http://php.net/imap_open))
* Get mailbox status (see [imap_check](http://php.net/imap_check))
* Receive emails (+attachments, +html body images)
* Search emails by custom criteria (see [imap_search](http://php.net/imap_search))
* Change email status (see [imap_setflag_full](http://php.net/imap_setflag_full))
* Delete email
### Installation by Composer
{
"require": {
"php-imap/php-imap": "~2.0"
}
}
Or
$ composer require php-imap/php-imap ~2.0
### Migration from `v1.*` to `v2.*`
Just add following code in the head of your script:
use PhpImap\Mailbox as ImapMailbox;
use PhpImap\IncomingMail;
use PhpImap\IncomingMailAttachment;
### [Usage example](https://github.com/barbushin/php-imap/blob/master/example/index.php)
```php
$mailbox = new PhpImap\Mailbox('{imap.gmail.com:993/imap/ssl}INBOX', 'some@gmail.com', '*********', __DIR__);
$mails = array();
$mailsIds = $mailbox->searchMailBox('ALL');
if(!$mailsIds) {
die('Mailbox is empty');
}
$mailId = reset($mailsIds);
$mail = $mailbox->getMail($mailId);
var_dump($mail);
var_dump($mail->getAttachments());
```
### Recommended
* Google Chrome extension [PHP Console](https://chrome.google.com/webstore/detail/php-console/nfhmhhlpfleoednkpnnnkolmclajemef)
* Google Chrome extension [JavaScript Errors Notifier](https://chrome.google.com/webstore/detail/javascript-errors-notifie/jafmfknfnkoekkdocjiaipcnmkklaajd)

View file

@ -0,0 +1,28 @@
{
"name": "php-imap/php-imap",
"description": "PHP class to access mailbox by POP3/IMAP/NNTP using IMAP extension",
"keywords": [
"PHP",
"IMAP",
"mail"
],
"homepage": "https://github.com/barbushin/php-imap",
"license": "BSD 3-Clause",
"type": "library",
"authors": [
{
"name": "Sergey Barbushin",
"homepage": "http://linkedin.com/in/barbushin",
"email": "barbushin@gmail.com"
}
],
"require": {
"php": ">=5.3.0"
},
"autoload": {
"psr-0": {
"PhpImap": "src/"
}
},
"minimum-stability": "stable"
}

View file

@ -0,0 +1,65 @@
<?php namespace PhpImap;
/**
* @see https://github.com/barbushin/php-imap
* @author Barbushin Sergey http://linkedin.com/in/barbushin
*/
class IncomingMail {
public $id;
public $date;
public $subject;
public $fromName;
public $fromAddress;
public $to = array();
public $toString;
public $cc = array();
public $replyTo = array();
public $messageId;
public $textPlain;
public $textHtml;
/** @var IncomingMailAttachment[] */
protected $attachments = array();
public function addAttachment(IncomingMailAttachment $attachment) {
$this->attachments[$attachment->id] = $attachment;
}
/**
* @return IncomingMailAttachment[]
*/
public function getAttachments() {
return $this->attachments;
}
/**
* Get array of internal HTML links placeholders
* @return array attachmentId => link placeholder
*/
public function getInternalLinksPlaceholders() {
return preg_match_all('/=["\'](ci?d:([\w\.%*@-]+))["\']/i', $this->textHtml, $matches) ? array_combine($matches[2], $matches[1]) : array();
}
public function replaceInternalLinks($baseUri) {
$baseUri = rtrim($baseUri, '\\/') . '/';
$fetchedHtml = $this->textHtml;
foreach($this->getInternalLinksPlaceholders() as $attachmentId => $placeholder) {
if(isset($this->attachments[$attachmentId])) {
$fetchedHtml = str_replace($placeholder, $baseUri . basename($this->attachments[$attachmentId]->filePath), $fetchedHtml);
}
}
return $fetchedHtml;
}
}
class IncomingMailAttachment {
public $id;
public $name;
public $filePath;
}

View file

@ -0,0 +1,611 @@
<?php namespace PhpImap;
use stdClass;
/**
* @see https://github.com/barbushin/php-imap
* @author Barbushin Sergey http://linkedin.com/in/barbushin
*/
class Mailbox {
protected $imapPath;
protected $imapLogin;
protected $imapPassword;
protected $imapOptions = 0;
protected $imapRetriesNum = 0;
protected $imapParams = array();
protected $serverEncoding;
protected $attachmentsDir;
public function __construct($imapPath, $login, $password, $attachmentsDir = null, $serverEncoding = 'UTF-8') {
$this->imapPath = $imapPath;
$this->imapLogin = $login;
$this->imapPassword = $password;
$this->serverEncoding = strtoupper($serverEncoding);
if($attachmentsDir) {
if(!is_dir($attachmentsDir)) {
throw new Exception('Directory "' . $attachmentsDir . '" not found');
}
$this->attachmentsDir = rtrim(realpath($attachmentsDir), '\\/');
}
}
/**
* Set custom connection arguments of imap_open method. See http://php.net/imap_open
* @param int $options
* @param int $retriesNum
* @param array $params
*/
public function setConnectionArgs($options = 0, $retriesNum = 0, array $params = null) {
$this->imapOptions = $options;
$this->imapRetriesNum = $retriesNum;
$this->imapParams = $params;
}
/**
* Get IMAP mailbox connection stream
* @param bool $forceConnection Initialize connection if it's not initialized
* @return null|resource
*/
public function getImapStream($forceConnection = true) {
static $imapStream;
if($forceConnection) {
if($imapStream && (!is_resource($imapStream) || !imap_ping($imapStream))) {
$this->disconnect();
$imapStream = null;
}
if(!$imapStream) {
$imapStream = $this->initImapStream();
}
}
return $imapStream;
}
protected function initImapStream() {
$imapStream = @imap_open($this->imapPath, $this->imapLogin, $this->imapPassword, $this->imapOptions, $this->imapRetriesNum, $this->imapParams);
if(!$imapStream) {
throw new Exception('Connection error: ' . imap_last_error());
}
return $imapStream;
}
protected function disconnect() {
$imapStream = $this->getImapStream(false);
if($imapStream && is_resource($imapStream)) {
imap_close($imapStream, CL_EXPUNGE);
}
}
/**
* Get information about the current mailbox.
*
* Returns the information in an object with following properties:
* Date - current system time formatted according to RFC2822
* Driver - protocol used to access this mailbox: POP3, IMAP, NNTP
* Mailbox - the mailbox name
* Nmsgs - number of mails in the mailbox
* Recent - number of recent mails in the mailbox
*
* @return stdClass
*/
public function checkMailbox() {
return imap_check($this->getImapStream());
}
/**
* Creates a new mailbox specified by mailbox.
*
* @return bool
*/
public function createMailbox() {
return imap_createmailbox($this->getImapStream(), imap_utf7_encode($this->imapPath));
}
/**
* Gets status information about the given mailbox.
*
* This function returns an object containing status information.
* The object has the following properties: messages, recent, unseen, uidnext, and uidvalidity.
*
* @return stdClass if the box doesn't exist
*/
public function statusMailbox() {
return imap_status($this->getImapStream(), $this->imapPath, SA_ALL);
}
/**
* Gets listing the folders
*
* This function returns an object containing listing the folders.
* The object has the following properties: messages, recent, unseen, uidnext, and uidvalidity.
*
* @return array listing the folders
*/
public function getListingFolders() {
$folders = imap_list($this->getImapStream(), $this->imapPath, "*");
foreach ($folders as $key => $folder)
{
$folder = str_replace($this->imapPath, "", imap_utf7_decode($folder));
$folders[$key] = $folder;
}
return $folders;
}
/**
* This function performs a search on the mailbox currently opened in the given IMAP stream.
* For example, to match all unanswered mails sent by Mom, you'd use: "UNANSWERED FROM mom".
* Searches appear to be case insensitive. This list of criteria is from a reading of the UW
* c-client source code and may be incomplete or inaccurate (see also RFC2060, section 6.4.4).
*
* @param string $criteria String, delimited by spaces, in which the following keywords are allowed. Any multi-word arguments (e.g. FROM "joey smith") must be quoted. Results will match all criteria entries.
* ALL - return all mails matching the rest of the criteria
* ANSWERED - match mails with the \\ANSWERED flag set
* BCC "string" - match mails with "string" in the Bcc: field
* BEFORE "date" - match mails with Date: before "date"
* BODY "string" - match mails with "string" in the body of the mail
* CC "string" - match mails with "string" in the Cc: field
* DELETED - match deleted mails
* FLAGGED - match mails with the \\FLAGGED (sometimes referred to as Important or Urgent) flag set
* FROM "string" - match mails with "string" in the From: field
* KEYWORD "string" - match mails with "string" as a keyword
* NEW - match new mails
* OLD - match old mails
* ON "date" - match mails with Date: matching "date"
* RECENT - match mails with the \\RECENT flag set
* SEEN - match mails that have been read (the \\SEEN flag is set)
* SINCE "date" - match mails with Date: after "date"
* SUBJECT "string" - match mails with "string" in the Subject:
* TEXT "string" - match mails with text "string"
* TO "string" - match mails with "string" in the To:
* UNANSWERED - match mails that have not been answered
* UNDELETED - match mails that are not deleted
* UNFLAGGED - match mails that are not flagged
* UNKEYWORD "string" - match mails that do not have the keyword "string"
* UNSEEN - match mails which have not been read yet
*
* @return array Mails ids
*/
public function searchMailbox($criteria = 'ALL') {
$mailsIds = imap_search($this->getImapStream(), $criteria, SE_UID, $this->serverEncoding);
return $mailsIds ? $mailsIds : array();
}
/**
* Save mail body.
* @return bool
*/
public function saveMail($mailId, $filename = 'email.eml') {
return imap_savebody($this->getImapStream(), $filename, $mailId, "", FT_UID);
}
/**
* Marks mails listed in mailId for deletion.
* @return bool
*/
public function deleteMail($mailId) {
return imap_delete($this->getImapStream(), $mailId, FT_UID);
}
public function moveMail($mailId, $mailBox) {
return imap_mail_move($this->getImapStream(), $mailId, $mailBox, CP_UID) && $this->expungeDeletedMails();
}
/**
* Deletes all the mails marked for deletion by imap_delete(), imap_mail_move(), or imap_setflag_full().
* @return bool
*/
public function expungeDeletedMails() {
return imap_expunge($this->getImapStream());
}
/**
* Add the flag \Seen to a mail.
* @return bool
*/
public function markMailAsRead($mailId) {
return $this->setFlag(array($mailId), '\\Seen');
}
/**
* Remove the flag \Seen from a mail.
* @return bool
*/
public function markMailAsUnread($mailId) {
return $this->clearFlag(array($mailId), '\\Seen');
}
/**
* Add the flag \Flagged to a mail.
* @return bool
*/
public function markMailAsImportant($mailId) {
return $this->setFlag(array($mailId), '\\Flagged');
}
/**
* Add the flag \Seen to a mails.
* @return bool
*/
public function markMailsAsRead(array $mailId) {
return $this->setFlag($mailId, '\\Seen');
}
/**
* Remove the flag \Seen from some mails.
* @return bool
*/
public function markMailsAsUnread(array $mailId) {
return $this->clearFlag($mailId, '\\Seen');
}
/**
* Add the flag \Flagged to some mails.
* @return bool
*/
public function markMailsAsImportant(array $mailId) {
return $this->setFlag($mailId, '\\Flagged');
}
/**
* Causes a store to add the specified flag to the flags set for the mails in the specified sequence.
*
* @param array $mailsIds
* @param string $flag which you can set are \Seen, \Answered, \Flagged, \Deleted, and \Draft as defined by RFC2060.
* @return bool
*/
public function setFlag(array $mailsIds, $flag) {
return imap_setflag_full($this->getImapStream(), implode(',', $mailsIds), $flag, ST_UID);
}
/**
* Cause a store to delete the specified flag to the flags set for the mails in the specified sequence.
*
* @param array $mailsIds
* @param string $flag which you can set are \Seen, \Answered, \Flagged, \Deleted, and \Draft as defined by RFC2060.
* @return bool
*/
public function clearFlag(array $mailsIds, $flag) {
return imap_clearflag_full($this->getImapStream(), implode(',', $mailsIds), $flag, ST_UID);
}
/**
* Fetch mail headers for listed mails ids
*
* Returns an array of objects describing one mail header each. The object will only define a property if it exists. The possible properties are:
* subject - the mails subject
* from - who sent it
* to - recipient
* date - when was it sent
* message_id - Mail-ID
* references - is a reference to this mail id
* in_reply_to - is a reply to this mail id
* size - size in bytes
* uid - UID the mail has in the mailbox
* msgno - mail sequence number in the mailbox
* recent - this mail is flagged as recent
* flagged - this mail is flagged
* answered - this mail is flagged as answered
* deleted - this mail is flagged for deletion
* seen - this mail is flagged as already read
* draft - this mail is flagged as being a draft
*
* @param array $mailsIds
* @return array
*/
public function getMailsInfo(array $mailsIds) {
$mails = imap_fetch_overview($this->getImapStream(), implode(',', $mailsIds), FT_UID);
if(is_array($mails) && count($mails))
{
foreach($mails as &$mail)
{
if(isset($mail->subject)) {
$mail->subject = $this->decodeMimeStr($mail->subject, $this->serverEncoding);
}
if(isset($mail->from)) {
$mail->from = $this->decodeMimeStr($mail->from, $this->serverEncoding);
}
if(isset($mail->to)) {
$mail->to = $this->decodeMimeStr($mail->to, $this->serverEncoding);
}
}
}
return $mails;
}
/**
* Get information about the current mailbox.
*
* Returns an object with following properties:
* Date - last change (current datetime)
* Driver - driver
* Mailbox - name of the mailbox
* Nmsgs - number of messages
* Recent - number of recent messages
* Unread - number of unread messages
* Deleted - number of deleted messages
* Size - mailbox size
*
* @return object Object with info | FALSE on failure
*/
public function getMailboxInfo() {
return imap_mailboxmsginfo($this->getImapStream());
}
/**
* Gets mails ids sorted by some criteria
*
* Criteria can be one (and only one) of the following constants:
* SORTDATE - mail Date
* SORTARRIVAL - arrival date (default)
* SORTFROM - mailbox in first From address
* SORTSUBJECT - mail subject
* SORTTO - mailbox in first To address
* SORTCC - mailbox in first cc address
* SORTSIZE - size of mail in octets
*
* @param int $criteria
* @param bool $reverse
* @return array Mails ids
*/
public function sortMails($criteria = SORTARRIVAL, $reverse = true) {
return imap_sort($this->getImapStream(), $criteria, $reverse, SE_UID);
}
/**
* Get mails count in mail box
* @return int
*/
public function countMails() {
return imap_num_msg($this->getImapStream());
}
/**
* Retrieve the quota settings per user
* @return array - FALSE in the case of call failure
*/
protected function getQuota() {
return imap_get_quotaroot($this->getImapStream(), 'INBOX');
}
/**
* Return quota limit in KB
* @return int - FALSE in the case of call failure
*/
public function getQuotaLimit() {
$quota = $this->getQuota();
if(is_array($quota)) {
$quota = $quota['STORAGE']['limit'];
}
return $quota;
}
/**
* Return quota usage in KB
* @return int - FALSE in the case of call failure
*/
public function getQuotaUsage() {
$quota = $this->getQuota();
if(is_array($quota)) {
$quota = $quota['STORAGE']['usage'];
}
return $quota;
}
/**
* Get mail data
*
* @param $mailId
* @param bool $markAsSeen
* @return IncomingMail
*/
public function getMail($mailId, $markAsSeen = true) {
$head = imap_rfc822_parse_headers(imap_fetchheader($this->getImapStream(), $mailId, FT_UID));
$mail = new IncomingMail();
$mail->id = $mailId;
$mail->date = date('Y-m-d H:i:s', isset($head->date) ? strtotime(preg_replace('/\(.*?\)/', '', $head->date)) : time());
$mail->subject = isset($head->subject) ? $this->decodeMimeStr($head->subject, $this->serverEncoding) : null;
$mail->fromName = isset($head->from[0]->personal) ? $this->decodeMimeStr($head->from[0]->personal, $this->serverEncoding) : null;
$mail->fromAddress = strtolower($head->from[0]->mailbox . '@' . $head->from[0]->host);
if(isset($head->to)) {
$toStrings = array();
foreach($head->to as $to) {
if(!empty($to->mailbox) && !empty($to->host)) {
$toEmail = strtolower($to->mailbox . '@' . $to->host);
$toName = isset($to->personal) ? $this->decodeMimeStr($to->personal, $this->serverEncoding) : null;
$toStrings[] = $toName ? "$toName <$toEmail>" : $toEmail;
$mail->to[$toEmail] = $toName;
}
}
$mail->toString = implode(', ', $toStrings);
}
if(isset($head->cc)) {
foreach($head->cc as $cc) {
$mail->cc[strtolower($cc->mailbox . '@' . $cc->host)] = isset($cc->personal) ? $this->decodeMimeStr($cc->personal, $this->serverEncoding) : null;
}
}
if(isset($head->reply_to)) {
foreach($head->reply_to as $replyTo) {
$mail->replyTo[strtolower($replyTo->mailbox . '@' . $replyTo->host)] = isset($replyTo->personal) ? $this->decodeMimeStr($replyTo->personal, $this->serverEncoding) : null;
}
}
if(isset($head->message_id)) {
$mail->messageId = $head->message_id;
}
$mailStructure = imap_fetchstructure($this->getImapStream(), $mailId, FT_UID);
if(empty($mailStructure->parts)) {
$this->initMailPart($mail, $mailStructure, 0, $markAsSeen);
}
else {
foreach($mailStructure->parts as $partNum => $partStructure) {
$this->initMailPart($mail, $partStructure, $partNum + 1, $markAsSeen);
}
}
return $mail;
}
protected function initMailPart(IncomingMail $mail, $partStructure, $partNum, $markAsSeen = true) {
$options = FT_UID;
if(!$markAsSeen) {
$options |= FT_PEEK;
}
$data = $partNum ? imap_fetchbody($this->getImapStream(), $mail->id, $partNum, $options) : imap_body($this->getImapStream(), $mail->id, $options);
if($partStructure->encoding == 1) {
$data = imap_utf8($data);
}
elseif($partStructure->encoding == 2) {
$data = imap_binary($data);
}
elseif($partStructure->encoding == 3) {
$data = preg_replace('~[^a-zA-Z0-9+=/]+~s', '', $data); // https://github.com/barbushin/php-imap/issues/88
$data = imap_base64($data);
}
elseif($partStructure->encoding == 4) {
$data = quoted_printable_decode($data);
}
$params = array();
if(!empty($partStructure->parameters)) {
foreach($partStructure->parameters as $param) {
$params[strtolower($param->attribute)] = $param->value;
}
}
if(!empty($partStructure->dparameters)) {
foreach($partStructure->dparameters as $param) {
$paramName = strtolower(preg_match('~^(.*?)\*~', $param->attribute, $matches) ? $matches[1] : $param->attribute);
if(isset($params[$paramName])) {
$params[$paramName] .= $param->value;
}
else {
$params[$paramName] = $param->value;
}
}
}
// attachments
$attachmentId = $partStructure->ifid
? trim($partStructure->id, " <>")
: (isset($params['filename']) || isset($params['name']) ? mt_rand() . mt_rand() : null);
if($attachmentId) {
if(empty($params['filename']) && empty($params['name'])) {
$fileName = $attachmentId . '.' . strtolower($partStructure->subtype);
}
else {
$fileName = !empty($params['filename']) ? $params['filename'] : $params['name'];
$fileName = $this->decodeMimeStr($fileName, $this->serverEncoding);
$fileName = $this->decodeRFC2231($fileName, $this->serverEncoding);
}
$attachment = new IncomingMailAttachment();
$attachment->id = $attachmentId;
$attachment->name = $fileName;
if($this->attachmentsDir) {
$replace = array(
'/\s/' => '_',
'/[^0-9a-zаіїє_\.]/iu' => '',
'/_+/' => '_',
'/(^_)|(_$)/' => '',
);
$fileSysName = preg_replace('~[\\\\/]~', '', $mail->id . '_' . $attachmentId . '_' . preg_replace(array_keys($replace), $replace, $fileName));
$attachment->filePath = $this->attachmentsDir . DIRECTORY_SEPARATOR . $fileSysName;
file_put_contents($attachment->filePath, $data);
}
$mail->addAttachment($attachment);
}
else {
if(!empty($params['charset'])) {
$data = $this->convertStringEncoding($data, $params['charset'], $this->serverEncoding);
}
if($partStructure->type == 0 && $data) {
if(strtolower($partStructure->subtype) == 'plain') {
$mail->textPlain .= $data;
}
else {
$mail->textHtml .= $data;
}
}
elseif($partStructure->type == 2 && $data) {
$mail->textPlain .= trim($data);
}
}
if(!empty($partStructure->parts)) {
foreach($partStructure->parts as $subPartNum => $subPartStructure) {
if($partStructure->type == 2 && $partStructure->subtype == 'RFC822') {
$this->initMailPart($mail, $subPartStructure, $partNum, $markAsSeen);
}
else {
$this->initMailPart($mail, $subPartStructure, $partNum . '.' . ($subPartNum + 1), $markAsSeen);
}
}
}
}
protected function decodeMimeStr($string, $charset = 'utf-8') {
$newString = '';
$elements = imap_mime_header_decode($string);
for($i = 0; $i < count($elements); $i++) {
if($elements[$i]->charset == 'default') {
$elements[$i]->charset = 'iso-8859-1';
}
$newString .= $this->convertStringEncoding($elements[$i]->text, $elements[$i]->charset, $charset);
}
return $newString;
}
function isUrlEncoded($string) {
$hasInvalidChars = preg_match( '#[^%a-zA-Z0-9\-_\.\+]#', $string );
$hasEscapedChars = preg_match( '#%[a-zA-Z0-9]{2}#', $string );
return !$hasInvalidChars && $hasEscapedChars;
}
protected function decodeRFC2231($string, $charset = 'utf-8') {
if(preg_match("/^(.*?)'.*?'(.*?)$/", $string, $matches)) {
$encoding = $matches[1];
$data = $matches[2];
if($this->isUrlEncoded($data)) {
$string = $this->convertStringEncoding(urldecode($data), $encoding, $charset);
}
}
return $string;
}
/**
* Converts a string from one encoding to another.
* @param string $string
* @param string $fromEncoding
* @param string $toEncoding
* @return string Converted string if conversion was successful, or the original string if not
*/
protected function convertStringEncoding($string, $fromEncoding, $toEncoding) {
$convertedString = null;
if($string && $fromEncoding != $toEncoding) {
$convertedString = @iconv($fromEncoding, $toEncoding . '//IGNORE', $string);
if(!$convertedString && extension_loaded('mbstring')) {
$convertedString = @mb_convert_encoding($string, $toEncoding, $fromEncoding);
}
}
return $convertedString ?: $string;
}
public function __destruct() {
$this->disconnect();
}
}
class Exception extends \Exception {
}

View file

@ -0,0 +1,8 @@
<?php namespace PhpImap;
spl_autoload_register(function ($class) {
if(strpos($class, __NAMESPACE__) === 0) {
/** @noinspection PhpIncludeInspection */
require_once(__DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR . str_replace('\\', DIRECTORY_SEPARATOR, $class) . '.php');
}
});

113
dist/backend.php vendored Normal file
View file

@ -0,0 +1,113 @@
<?php
require_once './config.php';
# load php dependencies:
require_once './backend-libs/autoload.php';
$imap_settings = $config['imap'];
$mailbox = new PhpImap\Mailbox($imap_settings['url'], $imap_settings['username'], $imap_settings['password']);
/**
* print error and stop program.
* @param $status http status
* @param $text error text
*/
function error($status, $text) {
@http_response_code($status);
@print("{\"error\": \"$text\"}");
die();
}
/**
* print all mails for the given $user as a json string.
* @param $username
*/
function print_inbox($username) {
global $mailbox, $config;
$name = clean_name($username);
if (strlen($name) === 0) {
error(400, 'invalid username');
}
$to = get_address($name, $config['mailHostname']);
$mail_ids = search_mails($to, $mailbox);
$emails = array();
foreach ($mail_ids as $id) {
$emails[] = $mailbox->getMail($id);
}
$address = get_address($name, $config['mailHostname']);
$data = array("mails" => $emails, 'username' => $name, 'address' => $address);
print(json_encode($data));
}
/**
* Search for mails with the recipient $to.
* @return array mail ids
*/
function search_mails($to, $mailbox) {
$filterTO = 'TO "' . $to . '"';
$filterCC = 'CC "' . $to . '"';
$mailsIdsTo = imap_sort($mailbox->getImapStream(), SORTARRIVAL, true, SE_UID, $filterTO);
$mailsIdsCc = imap_sort($mailbox->getImapStream(), SORTARRIVAL, true, SE_UID, $filterCC);
return array_merge($mailsIdsTo, $mailsIdsCc);
}
/**
* Remove illegal characters from username and remove everything after the @-sign. You may extend it if your server supports them.
* @param $username
* @return clean username
*/
function clean_name($username) {
$username = preg_replace('/@.*$/', "", $username); // remove part after @
$username = preg_replace('/[^A-Za-z0-9_.+-]/', "", $username); // remove special characters
return $username;
}
/**
* creates the full email address
* @param $username
* @param $domain
* @return $username@$domain
*/
function get_address($username, $domain) {
return $username . "@" . $domain;
}
/**
* deletes messages older than X days.
*/
function delete_old_messages() {
global $mailbox;
$date = date('d-M-Y', strtotime('30 days ago'));
$ids = $mailbox->searchMailbox('BEFORE ' . $date);
foreach ($ids as $id) {
$mailbox->deleteMail($id);
}
$mailbox->expungeDeletedMails();
}
header('Content-type: application/json');
// Never cache requests:
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
if (!isset($_GET['action'])) {
error(400, 'invalid parameter');
}
$action = $_GET['action'];
if ($action === "get" && isset($_GET['username'])) {
print_inbox($_GET['username']);
} else {
error(400, 'invalid action');
}
// run on every request
delete_old_messages();

15
dist/config.sample.php vendored Normal file
View file

@ -0,0 +1,15 @@
<?php
date_default_timezone_set('Europe/Paris');
error_reporting(0);
// configure this option if you want to allow requests from clients from other domains:
// see https://en.wikipedia.org/wiki/Cross-origin_resource_sharing
// header("Access-Control-Allow-Origin: *");
// setup imap connection
$config['imap']['host'] = "localhost";
$config['imap']['url'] = '{' . $config['imap']['host'] . '/imap/ssl}INBOX';
$config['imap']['username'] = "test";
$config['imap']['password'] = "test";
$config['mailHostname'] = "example.com";

49
dist/index.html vendored Normal file

File diff suppressed because one or more lines are too long

80
gulpfile.babel.js Normal file
View file

@ -0,0 +1,80 @@
import gulp from 'gulp';
import path from 'path';
import webpack from 'webpack';
import WebpackDevServer from 'webpack-dev-server';
import karma from 'karma';
import ip from 'ip';
import webpackConfig from './webpack.config';
import fileInline from 'gulp-file-inline';
let paths = {
build: path.join(__dirname, 'target/build')
};
/**
* Gulp-Task: Fuehrt webpack aus und startet den Development-Server
*/
gulp.task('dev-server', () => {
return new WebpackDevServer(webpack(webpackConfig.development), {
hot: true,
contentBase: './dist/',
watchOptions: {
aggregateTimeout: 100, poll: 300
}, stats: {
colors: true
}
}).listen(3000, 'localhost', function (err) {
if (err) {
console.error(err);
}
});
});
/**
* Gulp-Task: Fuehrt die Karma-Tests auf dem PhantomJS Browser aus
*/
gulp.task('test-phantomjs', (done) => {
let hostname = process.env.host || ip.address();
let externalport = process.env.externalport || 7777;
return new karma.Server({
configFile: __dirname + '/karma.conf.js',
hostname: hostname,
port: externalport,
browsers: ['PhantomJS']
}, done).start();
});
gulp.task('webpack-prod', [], (done) => {
return webpack(webpackConfig.production, done);
});
let inlineScript = function () {
return gulp
.src(path.join('target', 'build', 'index.html'))
.pipe(fileInline({
css: {
minify: false
},
js: {
minify: false
}
}))
.pipe(gulp.dest('dist'))
};
gulp.task('build', ['test-phantomjs', 'webpack-prod'], (done) => {
return inlineScript();
});
gulp.task('build-skipTests', ['webpack-prod'], (done) => {
return inlineScript();
});
gulp.task('default', ['dev-server']);

112
karma.conf.js Normal file
View file

@ -0,0 +1,112 @@
var ip = require('ip');
var webjsConfig = require('./shared.build.config');
module.exports = function (config) {
var seleniumWebgrid = {
hostname: 'webtestgrid.myhost.com',
port: 4444
};
config.set({
hostname: ip.address(),
basePath: __dirname,
// frameworks to use
// available frameworks: https://npmjs.org/browse/keyword/karma-adapter
frameworks: ['jasmine'],
/* list of files/patterns to load in the browser
Fuer den Phantomjs wird fuer die Methode 'bind' ein Polyfill geladen
da der Browser die Methode nicht kennt und diese von den Frameworks verwendet wird */
files: [
'./node_modules/phantomjs-polyfill/bind-polyfill.js', {
pattern: 'spec.bundle.js', watched: false
}
],
// files to exclude
exclude: [],
// preprocess matching files before serving them to the browser
// available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
preprocessors: {
'spec.bundle.js': ['webpack', 'sourcemap', 'coverage']
},
webpack: {
debug: true, devtool: 'inline-source-map', module: {
loaders: webjsConfig.webpackLoaders
}
},
webpackServer: {
// prevent console spamming when running in Karma!
noInfo: true
},
// available reporters: https://npmjs.org/browse/keyword/karma-reporter
reporters: ['progress', 'junit', 'coverage'],
// enable colors in the output
colors: true,
// level of logging
// possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
logLevel: config.LOG_INFO,
// toggle whether to watch files and rerun tests upon incurring changes
autoWatch: false,
// Browser-Konfiguration auf dem Selenium Grid
customLaunchers: {
'SeleniumCH': {
base: 'WebDriver',
config: seleniumWebgrid,
browserName: 'chrome'
},
'SeleniumFF': {
base: 'WebDriver',
config: seleniumWebgrid,
browserName: 'firefox'
},
'SeleniumIE': {
base: 'WebDriver',
config: seleniumWebgrid,
browserName: 'internet explorer',
'x-ua-compatible': 'IE=edge'
}
},
// start these browsers
// available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
browsers: ['PhantomJS'], // Test auf dem PhantomJS
// browsers: ['SeleniumFF', 'SeleniumCH', 'SeleniumIE'], // Test auf dem Selenium-Webgrid
// if true, Karma runs tests once and exits
singleRun: true,
plugins: [
'karma-junit-reporter',
'karma-jasmine',
'karma-coverage',
'karma-phantomjs-launcher',
'karma-webpack',
'karma-sourcemap-loader',
'karma-webdriver-launcher'
],
// Coverage & JUnit Report fuer SonarQube
junitReporter: {
outputDir: 'target/surefire', suite: 'unit'
}, coverageReporter: {
reporters: [
{
type: 'lcov', dir: 'target/surefire', subdir: '.'
}
]
}
});
};

60
package.json Normal file
View file

@ -0,0 +1,60 @@
{
"name": "disposable-mailbox",
"version": "0.0.1",
"description": "disposable-mailbox",
"homepage": "https://github.com/synox/disposable-mailbox",
"dependencies": {
"angular": "^1.5.2",
"angular-resource": "^1.5.2",
"angular-sanitize": "^1.5.6",
"angular-ui-router": "^1.0.0-beta.1",
"angular-ui-bootstrap": "^1.3.3",
"autolinker": "^0.27.0",
"babel-polyfill": "^6.9.1",
"bootstrap": "^3.3.6",
"chance": "^1.0.3"
},
"devDependencies": {
"angular-mocks": "^1.5.2",
"babel-core": "^6.10.4",
"babel-loader": "^6.2.4",
"babel-preset-es2015": "^6.9.0",
"browser-sync": "^2.10.0",
"browser-sync-webpack-plugin": "^1.0.0",
"css-loader": "^0.23.1",
"file-loader": "^0.9.0",
"gulp": "^3.9.0",
"gulp-esdoc": "^0.2.0",
"gulp-file-inline": "^1.3.6",
"html-loader": "^0.4.3",
"html-webpack-plugin": "^2.15.0",
"ip": "^1.0.2",
"isparta-loader": "^2.0.0",
"jasmine-core": "^2.3.4",
"json-loader": "^0.5.3",
"karma": "^1.1.0",
"karma-coverage": "^1.0.0",
"karma-jasmine": "~1.0.2",
"karma-junit-reporter": "^1.1.0",
"karma-phantomjs-launcher": "^1.0.1",
"karma-sourcemap-loader": "^0.3.4",
"karma-webdriver-launcher": "^1.0.4",
"karma-webpack": "^1.5.1",
"ng-annotate-webpack-plugin": "^0.1.2",
"node-libs-browser": "^1.0.0",
"node.extend": "^1.1.5",
"phantomjs": "^2.1.7",
"phantomjs-polyfill": "0.0.2",
"proxy-middleware": "^0.15.0",
"raw-loader": "^0.5.1",
"style-loader": "^0.13.0",
"url-loader": "^0.5.6",
"webpack": "^1.9.5",
"webpack-dev-server": "^1.12.1"
},
"repository": {
"type": "git",
"url": "https://github.com/synox/disposable-mailbox.git"
},
"license": "CC-BY-NC-SA-4.0"
}

65
readme.md Normal file
View file

@ -0,0 +1,65 @@
# self-hosted disposable email service
## Goals:
* easy to use: generate random name or use custom name, auto refresh
* easy to host: just php5 + imap extension
* easy to install: just copy files in `dist`
* minimal code base: minimal features and complexity
![screenshot](screenshot.png)
## Quality/Status:
This is **alpha-tested** software, do not use it in production yes, it may lose your mails and people may gain access to your mails. There are still unsolved problems. Contributions are welcome!
## Licence
<a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/"><img alt="Creative Commons License" style="border-width:0" src="https://i.creativecommons.org/l/by-nc-sa/4.0/88x31.png" /></a><br /><span xmlns:dct="http://purl.org/dc/terms/" property="dct:title">disposable-mailbox</span> by <a xmlns:cc="http://creativecommons.org/ns#" href="https://github.com/synox/disposable-mailbox" property="cc:attributionName" rel="cc:attributionURL">github:synox</a> is licensed under a <a rel="license" href="http://creativecommons.org/licenses/by-nc-sa/4.0/">Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International License</a>.
## Webserver requirements
* php >=5.3.0
* [imap extension](http://php.net/manual/book.imap.php)
* apache 2 (but should work on any webserver)
## Installation
1. assure the [imap extension](http://php.net/manual/book.imap.php) is installed. The following command should not print any errors:
<?php print imap_base64("SU1BUCBleHRlbnNpb24gc2VlbXMgdG8gYmUgaW5zdGFsbGVkLiA="); ?>
2. clone or download this repository
3. copy the `dist` directory to your web server.
4. rename `config.sample.php` to `config.php` and apply the imap settings. Move `config.php` to a safe location outside the `public_html`.
5. open `backend.php` and set the new path to `config.php`.
## Build it yourself
Instead of using the files in the `dist` directory you can also build it yourself. You must have [npm](https://docs.npmjs.com/cli/install) and [composer](https://getcomposer.org/download/) installed.
Install php dependecies:
composer install
Install javascript dependencies:
npm install
Build frontend:
gulp build
The files are written to the `dist` directory.
There is a [Vagrantfile](Vagrantfile), in case you are familiar with [vagrant](https://www.vagrantup.com/).
## TODO
1. reduce total dist size (<1 MB)
1. setup a link redirection provider (to keep the existence of your installation secret)
1. maybe make mails collapsible
## Credit
This could not be possible without...
* http://angularjs.org/
* https://github.com/SchweizerischeBundesbahnen/esta-webjs
* https://github.com/barbushin/php-imap

BIN
screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

31
shared.build.config.js Normal file
View file

@ -0,0 +1,31 @@
/*
* @author u215942 (Stefan Zeller)
* @version: 1.0.1
* @since 04.04.2016
*/
exports.webpackLoaders = [
{
test: /\.js$/, exclude: [/node_modules/],
loader: 'babel',
query: {
// https://github.com/babel/babel-loader#options
cacheDirectory: true,
presets: ['es2015']
}
}, {
test: /\.json$/, loader: 'json'
}, {
test: /\.html$/, loader: 'html'
}, {
test: /\.css$/, loader: 'style!css'
}, {
test: /\.(jpe?g|png|gif|svg)$/i, loader: 'url'
}, {
test: /\.(woff|woff2)$/, loader: 'url?mimetype=application/font-woff'
}, {
test: /\.ttf$/, loader: 'url'
}, {
test: /\.eot$/, loader: 'url'
}
];

5
spec.bundle.js Normal file
View file

@ -0,0 +1,5 @@
import 'angular';
import 'angular-mocks';
let context = require.context('./src/app', true, /\.spec\.js/);
context.keys().forEach(context);

11
src/app/app.component.js Normal file
View file

@ -0,0 +1,11 @@
import template from './app.html';
import './app.css';
let appComponent = () => {
return {
template,
restrict: 'E'
};
};
export default appComponent;

8
src/app/app.css Normal file
View file

@ -0,0 +1,8 @@
body {
background: #eeeeee;
}
footer p {
margin-top: 50px;
text-align: center;
}

1
src/app/app.html Normal file
View file

@ -0,0 +1 @@
<div ui-view></div>

22
src/app/app.js Normal file
View file

@ -0,0 +1,22 @@
// Vendor-Imports
import angular from 'angular';
import uiRouter from 'angular-ui-router';
import ngResource from 'angular-resource';
import uiBootstrap from 'angular-ui-bootstrap';
import 'bootstrap/dist/css/bootstrap.css';
import 'babel-polyfill';
// Interne Modul-Imports
import Mailbox from './mailbox/mailbox';
import Navbar from './navbar/navbar';
import AppComponent from './app.component';
angular.module('app', [
uiRouter, ngResource, uiBootstrap, Mailbox.name, Navbar.name
])
.constant('config', {
'backend_url': './backend.php',
'reload_interval_ms': 10000
})
.directive('app', AppComponent);

View file

@ -0,0 +1,8 @@
class HomeController {
/*@ngInject*/
constructor() {
}
}
export default HomeController;

View file

@ -0,0 +1,3 @@
div.home {
min-height: 400px;
}

View file

@ -0,0 +1,7 @@
<navbar></navbar>
<main>
<div class="container home">
Use the buttons above to create a new inbox, or open a specific mailbox.
</div>
</main>

View file

@ -0,0 +1,9 @@
import angular from 'angular';
import uiRouter from 'angular-ui-router';
import template from './home.html';
import controller from './home.controller';
import './home.css';
export default angular.module('mailbox.home', [uiRouter])
.component('home', {template, controller})

View file

@ -0,0 +1,37 @@
class MailboxController {
/*@ngInject*/
constructor($log, $interval, config, mailboxService) {
this.$log = $log;
this.$interval = $interval;
this.config = config;
this.mailboxService = mailboxService;
this.loadingData = true;
this.mails = [];
this.address = null;
}
$onInit() {
this.username = this.mailboxService.getCurrentUsername();
this.intervalPromise = this.$interval(() => this.loadMails(), this.config.reload_interval_ms);
this.loadMails();
}
$onDestroy() {
this.$log.debug("destroying controller");
this.$interval.cancel(this.intervalPromise);
}
loadMails() {
this.mailboxService.loadEmails(this.username)
.then(data => {
this.mails = data.mails;
this.address = this.mailboxService.getCurrentAddress();
this.loadingData = false;
});
}
}
export default MailboxController;

View file

@ -0,0 +1,12 @@
.email-table {
margin-top: 20px;
background: white;
}
.waiting-screen {
padding: 40px 15px;
text-align: center;
}

View file

@ -0,0 +1,28 @@
<navbar></navbar>
<main>
<div class="container">
<div ng-show="$ctrl.loadingData" class="waiting-screen">
<h1>&nbsp;</h1>
<p class="lead">Loading Mails</p>
<p><br/>
<img src="spinner.gif">
<br/>
</p>
</div>
<div ng-hide="$ctrl.loadingData">
<div ng-repeat="mail in $ctrl.mails | orderBy:'-date' track by $index" class="email-table">
<mail mail="mail"></mail>
</div>
<div class="waiting-screen" ng-show="$ctrl.mails.length === 0">
<h1>{{$ctrl.address}}</h1>
<p class="lead">Inbox is empty.</p>
<p><br/>
<img src="spinner.gif">
<br/></p>
<p class="lead">Emails to {{address}} will be automatically displayed on this page. </p>
</div>
</div>
</div>
</main>

View file

@ -0,0 +1,16 @@
import angular from 'angular';
import uiRouter from 'angular-ui-router';
import template from './inbox.html';
import controller from './inbox.controller';
import './inbox.css';
import Mail from './mail/mail'
export default angular.module('mailbox.inbox', [uiRouter, Mail.name])
.component('inbox', {
template,
controller,
bindings: {
data: '<'
}
})

View file

@ -0,0 +1,4 @@
describe('not tests', () => {
it('should run anyway', () => {
});
});

View file

@ -0,0 +1,29 @@
class MailController {
/*@ngInject*/
constructor(mailboxService) {
this.mailboxService = mailboxService;
this.deleted = false;
this.displayMode = 'text'
}
deleteMail(id) {
this.mailboxService.deleteMail(id)
.then(()=> {
this.deleted = true;
});
}
showTextButton() {
if ( this.mail.textPlain && !this.mail.textHtml){
return false;
} else {
return true;
}
}
showHtmlButton() {
return !! this.mail.textHtml;
}
}
export default MailController;

View file

@ -0,0 +1,39 @@
.email-table .email-header {
font-weight: bold;
border-top: 5px solid #7C96AB;
border-bottom: 1px solid #7C96AB;
padding: 10px;
background-color: white;
}
.email-table .email-info {
background-color: white;
}
.email-table .email-body {
overflow: hidden;
background-color: white;
}
.info-list {
margin: 0 0 21px;
border-bottom: 1px solid #ccc;
padding: 0 0 16px;
color: #999;
overflow: hidden;
}
.info-list dt {
float: left;
color: #666;
margin: 0 3px 0 0;
}
.info-list dd {
display: block;
overflow: hidden;
}

View file

@ -0,0 +1,39 @@
<section ng-hide="$ctrl.deleted">
<div class="row email-header">
<div class="col-sm-4">{{$ctrl.mail.fromAddress}}</div>
<div class="col-sm-6">{{$ctrl.mail.subject}}</div>
<div class="col-sm-2">
<!--<button class="btn btn-primary" ng-click="$ctrl.deleteMail($ctrl.mail.id)">Delete</button>-->
</div>
</div>
<div class="row email-info">
<div class="col-sm-12">
<dl class="info-list">
<dt>To:</dt>
<dd>{{$ctrl.mail.toString}}</dd>
<div ng-if="$ctrl.mail.cc" ng-repeat="(address,name) in $ctrl.mail.cc">
<dt>CC:</dt>
<dd>{{address}}</dd>
</div>
<dt>From:</dt>
<dd>{{$ctrl.mail.fromName}} &lt;{{$ctrl.mail.fromAddress}}&gt;</dd>
<dt>Subject:</dt>
<dd>{{$ctrl.mail.subject}}</dd>
<dt>Date:</dt>
<dd>{{$ctrl.mail.date}}</dd>
</dl>
<label class="btn btn-primary" ng-show="$ctrl.showTextButton()" ng-model="$ctrl.displayMode"
uib-btn-radio="'text'">Text</label>
<label class="btn btn-primary" ng-show="$ctrl.showHtmlButton()" ng-model="$ctrl.displayMode"
uib-btn-radio="'html'">Html</label>
</div>
</div>
<div class="row email-body" ng-switch="$ctrl.displayMode">
<div class="col-sm-10" ng-switch-when="html" ng-bind-html="$ctrl.mail.textHtml"></div>
<div class="col-sm-10" ng-switch-when="text" ng-bind-html="$ctrl.mail.textPlain | nl2br | autolink "></div>
</div>
</section>

View file

@ -0,0 +1,33 @@
import angular from 'angular';
import uiRouter from 'angular-ui-router';
import ngSanitize from 'angular-sanitize';
import Autolinker from 'autolinker';
import template from './mail.html';
import controller from './mail.controller';
import './mail.css';
export default angular.module('mailbox.inbox.mail', [uiRouter, ngSanitize])
.component('mail', {
template,
controller,
bindings: {
mail: '='
}
})
// http://stackoverflow.com/a/20033625/79461
.filter("nl2br", function () {
return function (data) {
if (!data) return data;
return data.replace(/\r?\n/g, '<br/>');
}
}
)
// http://stackoverflow.com/a/20033625/79461
.filter("autolink", function () {
return function (data) {
return Autolinker.link(data, {truncate: {length: 50, location: 'middle', newWindow: true}});
}
}
);

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

View file

@ -0,0 +1,24 @@
import angular from 'angular';
import uiRouter from 'angular-ui-router';
import Service from './service/service';
import Home from './home/home';
import Inbox from './inbox/inbox';
let module = angular.module('mailbox', [uiRouter, Inbox.name, Service.name, Home.name])
.config(/*@ngInject*/($stateProvider, $urlRouterProvider) => {
$urlRouterProvider.otherwise('/');
$stateProvider.state('home', {
url: "/",
component: 'home'
});
$stateProvider.state('inbox', {
url: '/:username',
component: 'inbox'
});
});
export default module;

View file

@ -0,0 +1,82 @@
import Chance from 'chance';
class MailboxService {
/*@ngInject*/
constructor($http, $log, $state, $stateParams, config) {
this.name = 'mailboxService';
this.$http = $http;
this.$log = $log;
this.$state = $state;
this.$stateParams = $stateParams;
this.config = config;
this.address = null;
this.chance = new Chance();
}
openMailbox(username) {
username = MailboxService.cleanUsername(username);
this.setCurrentAddress(username);
this.$state.go('inbox', {username: username});
}
deleteMail(id) {
this.$log.info('deleting mails with id ' + id);
return this.$http.post(this.config.backend_url, {
params: {
id: id,
username: this.getCurrentUsername(),
action: "delete"
}
});
}
loadEmails(username) {
return this.$http.get(this.config.backend_url, {params: {username: username, action: "get"}})
.then(response=> {
this.setCurrentAddress(response.data.address);
return response.data;
}
);
}
static cleanUsername(username) {
return username.replace(/[@].*$/, '');
}
createMailbox() {
let username = this.generateRandomUsername();
this.openMailbox(username);
}
generateRandomUsername() {
let username = null;
if (this.chance.bool()) {
username = this.chance.word({syllables: 3});
} else {
username = this.chance.first();
}
if (this.chance.bool()) {
username += this.chance.integer({min: 50, max: 99});
}
if (this.chance.bool()) {
username += this.chance.tld();
}
username = username.toLowerCase();
return username;
}
getCurrentUsername() {
return this.$stateParams.username;
}
setCurrentAddress(address) {
this.address = address;
}
getCurrentAddress() {
return this.address
}
}
export default MailboxService;

View file

@ -0,0 +1,6 @@
import angular from 'angular';
import Service from './mailbox.service'
export default angular.module('mailbox.service', [])
.service('mailboxService', Service)

View file

@ -0,0 +1,29 @@
class NavbarController {
/*@ngInject*/
constructor(mailboxService, $stateParams, $rootScope) {
this.$rootScope = $rootScope;
this.mailboxService = mailboxService;
this.$stateParams = $stateParams;
}
$onInit() {
this.$rootScope.$watch(
()=> this.mailboxService.getCurrentAddress(),
(newValue, oldValue)=> {
this.address = newValue;
}
);
this.address = this.mailboxService.getCurrentAddress();
}
openMailbox(username) {
this.mailboxService.openMailbox(username);
}
createMailbox() {
this.mailboxService.createMailbox();
}
}
export default NavbarController;

11
src/app/navbar/navbar.css Normal file
View file

@ -0,0 +1,11 @@
.navbar {
background-color: #D9E2E9;
}
.octicon-inbox {
display: inline-block;
width: 26px;
height: 23px;
background: url('octicon-inbox.png') no-repeat;
}

View file

@ -0,0 +1,20 @@
<div class="navbar" role="navigation">
<div class="container">
<div class="navbar-header">
<a class="navbar-brand"><span class="octicon-inbox"></span> Mailbox</a>
</div>
<form class="navbar-form navbar-left">
<a class="btn btn-default" ng-click="$ctrl.createMailbox()" role="button">
<span class="glyphicon glyphicon-random" aria-hidden="true"></span>
create random
</a>
</form>
<form class="navbar-form navbar-left" role="search" ng-submit="$ctrl.openMailbox($ctrl.address)">
<input ng-model="$ctrl.address" type='text' class="form-control"/>
<button type="submit" class="btn btn-default">open</button>
</form>
</div>
</div>

14
src/app/navbar/navbar.js Normal file
View file

@ -0,0 +1,14 @@
import angular from 'angular';
import uiRouter from 'angular-ui-router';
import template from './navbar.html';
import controller from './navbar.controller';
import './navbar.css'
let navbarModule = angular.module('navbar', [uiRouter])
.component('navbar', {
template,
controller
});
export default navbarModule;

Binary file not shown.

After

Width:  |  Height:  |  Size: 7 KiB

113
src/backend.php Normal file
View file

@ -0,0 +1,113 @@
<?php
require_once './config.php';
# load php dependencies:
require_once './backend-libs/autoload.php';
$imap_settings = $config['imap'];
$mailbox = new PhpImap\Mailbox($imap_settings['url'], $imap_settings['username'], $imap_settings['password']);
/**
* print error and stop program.
* @param $status http status
* @param $text error text
*/
function error($status, $text) {
@http_response_code($status);
@print("{\"error\": \"$text\"}");
die();
}
/**
* print all mails for the given $user as a json string.
* @param $username
*/
function print_inbox($username) {
global $mailbox, $config;
$name = clean_name($username);
if (strlen($name) === 0) {
error(400, 'invalid username');
}
$to = get_address($name, $config['mailHostname']);
$mail_ids = search_mails($to, $mailbox);
$emails = array();
foreach ($mail_ids as $id) {
$emails[] = $mailbox->getMail($id);
}
$address = get_address($name, $config['mailHostname']);
$data = array("mails" => $emails, 'username' => $name, 'address' => $address);
print(json_encode($data));
}
/**
* Search for mails with the recipient $to.
* @return array mail ids
*/
function search_mails($to, $mailbox) {
$filterTO = 'TO "' . $to . '"';
$filterCC = 'CC "' . $to . '"';
$mailsIdsTo = imap_sort($mailbox->getImapStream(), SORTARRIVAL, true, SE_UID, $filterTO);
$mailsIdsCc = imap_sort($mailbox->getImapStream(), SORTARRIVAL, true, SE_UID, $filterCC);
return array_merge($mailsIdsTo, $mailsIdsCc);
}
/**
* Remove illegal characters from username and remove everything after the @-sign. You may extend it if your server supports them.
* @param $username
* @return clean username
*/
function clean_name($username) {
$username = preg_replace('/@.*$/', "", $username); // remove part after @
$username = preg_replace('/[^A-Za-z0-9_.+-]/', "", $username); // remove special characters
return $username;
}
/**
* creates the full email address
* @param $username
* @param $domain
* @return $username@$domain
*/
function get_address($username, $domain) {
return $username . "@" . $domain;
}
/**
* deletes messages older than X days.
*/
function delete_old_messages() {
global $mailbox;
$date = date('d-M-Y', strtotime('30 days ago'));
$ids = $mailbox->searchMailbox('BEFORE ' . $date);
foreach ($ids as $id) {
$mailbox->deleteMail($id);
}
$mailbox->expungeDeletedMails();
}
header('Content-type: application/json');
// Never cache requests:
header("Cache-Control: no-store, no-cache, must-revalidate, max-age=0");
header("Cache-Control: post-check=0, pre-check=0", false);
header("Pragma: no-cache");
if (!isset($_GET['action'])) {
error(400, 'invalid parameter');
}
$action = $_GET['action'];
if ($action === "get" && isset($_GET['username'])) {
print_inbox($_GET['username']);
} else {
error(400, 'invalid action');
}
// run on every request
delete_old_messages();

15
src/config.sample.php Normal file
View file

@ -0,0 +1,15 @@
<?php
date_default_timezone_set('Europe/Paris');
error_reporting(0);
// configure this option if you want to allow requests from clients from other domains:
// see https://en.wikipedia.org/wiki/Cross-origin_resource_sharing
// header("Access-Control-Allow-Origin: *");
// setup imap connection
$config['imap']['host'] = "localhost";
$config['imap']['url'] = '{' . $config['imap']['host'] . '/imap/ssl}INBOX';
$config['imap']['username'] = "test";
$config['imap']['password'] = "test";
$config['mailHostname'] = "example.com";

28
src/index.html Normal file
View file

@ -0,0 +1,28 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8">
<title>Mailbox</title>
<meta name="viewport" content="width=device-width initial-scale=1 maximum-scale=1 user-scalable=no">
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="description" content="Mailbox">
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
</head>
<body ng-app="app" ng-cloak>
<app>
Loading application...
</app>
<footer>
<p>Powered by <a href="https://github.com/synox/disposable-mailbox"><strong>synox/disposable-mailbox</strong></a>
| <a href="https://github.com/synox/disposable-mailbox"><span class="octicon octicon-mark-github"></span> Fork me on github</a></p>
</footer>
</body>
</html>

81
webpack.config.js Normal file
View file

@ -0,0 +1,81 @@
var webpack = require('webpack');
var extend = require('node.extend');
var path = require('path');
var browserSyncPlugin = require('browser-sync-webpack-plugin');
var ngAnnotatePlugin = require('ng-annotate-webpack-plugin');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var webjsConfig = require('./shared.build.config');
var url = require('url');
var proxyMiddleware = require('proxy-middleware');
/**
* Gemeinsame Konfigurationsdatei fuer Webpack (der Teil, der fuer alle Umgebungen gleich ist)
* @type {} Webpack Konfiguration
*/
var commonConfig = {
context: path.resolve(__dirname, 'src/app'),
// Einstiegspunkt fuer Webpack
entry: {
app: './app.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name]bundle.js'
},
// Modulkonfiguration fuer alle Dateitypen, welcher Loader soll verwendet werden
module: {
loaders: webjsConfig.webpackLoaders
},
resolve: {
fallback: path.join(__dirname, 'node_modules')
},
resolveLoader: {fallback: path.join(__dirname, 'node_modules')}
};
/**
* Production Konfigurationsdatei fuer Webpack (der Teil, der nur fuer den produktiven Build ist)
* @type {} Webpack Konfiguration
*/
var production = extend({}, commonConfig, {
output: {
path: path.join(__dirname, 'target/build'),
filename: '[name].js'
},
plugins: [
new ngAnnotatePlugin({add: true}),
new webpack.optimize.DedupePlugin(),
new webpack.optimize.UglifyJsPlugin({
minimize: true,
compress: {
warnings: true
},
sourceMap: false
}),
// automatisches Einfügen der Dateien app und vendor
new HtmlWebpackPlugin({
template: '../index.html'
})
]
});
// development config
// forward requests (you may also have to change "backend_url" in app.js
var proxyOptions = url.parse('http://localhost:8080');
proxyOptions.route = '/backend.php';
var development = extend({}, commonConfig, {
plugins: [
new browserSyncPlugin({
proxy: 'localhost:3000',
middleware: proxyMiddleware(proxyOptions)
}),
new HtmlWebpackPlugin({
template: '../index.html'
})
],
watch: true,
devtool: 'source-map'
});
module.exports = {production: production, development: development};