2014年9月28日 星期日

iOS 開發筆記 - iPhone Simulator Document Path

開發 iOS app 時,若有資料儲存時,常常會想直接去翻 iPhone Simulator 的真實儲存位置,久了就記住位置:

$ ls ~/Library/Application\ Support/iPhone\ Simulator/7.1/Applications/

最近改用 iOS 8 時,卻找不到 8.0 位置 XD 直到在 App 中印出位置才得知換了,粗略位置:

$ ls ~/Library/Developer/CoreSimulator/Devices/########-####-####-####-###########/data/Containers/Data/Application/

2014年9月22日 星期一

[OSX] 找尋 ISO 639 和 ISO 3166 定義的 language list @ Mac OS X 10.9.5

想說翻一下 wiki 又有點 ooxx... 偷問一下高手,高手非常直觀地跟我說,用一下 /usr/share/locale !

由於我是用 Mac OS X ,該產品語言已經做得很不錯了!果真的輕輕鬆鬆找出我要的東西:

$ ls /usr/share/locale | grep -v "\\." | grep "_"
af_ZA
am_ET
be_BY
bg_BG
ca_ES
cs_CZ
da_DK
de_AT
de_CH
de_DE
el_GR
en_AU
en_CA
en_GB
en_IE
en_NZ
en_US
es_ES
et_EE
eu_ES
fi_FI
fr_BE
fr_CA
fr_CH
fr_FR
he_IL
hr_HR
hu_HU
hy_AM
is_IS
it_CH
it_IT
ja_JP
kk_KZ
ko_KR
lt_LT
nl_BE
nl_NL
no_NO
pl_PL
pt_BR
pt_PT
ro_RO
ru_RU
sk_SK
sl_SI
sr_YU
sv_SE
tr_TR
uk_UA
zh_CN
zh_HK
zh_TW


高手獻技:

$ cd /usr/share/locale && echo ??_??
af_ZA am_ET be_BY bg_BG ca_ES cs_CZ da_DK de_AT de_CH de_DE el_GR en_AU en_CA en_GB en_IE en_NZ en_US es_ES et_EE eu_ES fi_FI fr_BE fr_CA fr_CH fr_FR he_IL hr_HR hu_HU hy_AM is_IS it_CH it_IT ja_JP kk_KZ ko_KR lt_LT nl_BE nl_NL no_NO pl_PL pt_BR pt_PT ro_RO ru_RU sk_SK sl_SI sr_YU sv_SE tr_TR uk_UA zh_CN zh_HK zh_TW


後來又找到對應表: http://www.localeplanet.com/icu/

<?php
// http://www.localeplanet.com/icu/
$lang_map = array(
        'af_ZA' => 'Afrikaans (Suid-Afrika)',
        'am_ET' => 'አማርኛ (ኢትዮጵያ)' ,
        'be_BY' => 'беларуская (Беларусь)' ,
        'bg_BG' => 'български (България)',
        'ca_ES' => 'català (Espanya)',
        'cs_CZ' => 'čeština (Česká republika)',
        'da_DK' => 'dansk (Danmark)',
        'de_AT' => 'Deutsch (Österreich)',
        'de_CH' => 'Deutsch (Schweiz)',
        'de_DE' => 'Deutsch (Deutschland)',
        'el_GR' => 'Ελληνικά (Ελλάδα)',
        'en_AU' => 'English (Australia)',
        'en_CA' => 'English (Canada)',
        'en_GB' => 'English (United Kingdom)',
        'en_IE' => 'English (Ireland)',
        'en_NZ' => 'English (New Zealand)',
        'en_US' => 'English (United States)',
        'es_ES' => 'español (España)',
        'et_EE' => 'eesti (Eesti)' ,
        'eu_ES' => 'euskara (Espainia)',
        'fi_FI' => 'suomi (Suomi)',
        'fr_BE' => 'français (Belgique)',
        'fr_CA' => 'français (Canada)',
        'fr_CH' => 'français (Suisse)',
        'fr_FR' => 'français (France)',
        'he_IL' => 'עברית (ישראל)',
        'hr_HR' => 'hrvatski (Hrvatska)',
        'hu_HU' => 'magyar (Magyarország)',
        'hy_AM' => 'Հայերէն (Հայաստանի Հանրապետութիւն)',
        'is_IS' => 'íslenska (Ísland)',
        'it_CH' => 'italiano (Svizzera)' ,
        'it_IT' => 'italiano (Italia)',
        'ja_JP' => '日本語(日本)',
        'kk_KZ' => 'kk_KZ',
        'ko_KR' => '한국어(대한민국)',
        'lt_LT' => 'lietuvių (Lietuva)',
        'nl_BE' => 'Nederlands (België)',
        'nl_NL' => 'Nederlands (Nederland)',
        'no_NO' => 'no_NO',
        'pl_PL' => 'polski (Polska)',
        'pt_BR' => 'português (Brasil)',
        'pt_PT' => 'português (Portugal)',
        'ro_RO' => 'română (România)',
        'ru_RU' => 'русский (Россия)',
        'sk_SK' => 'slovenčina (Slovenská republika)',
        'sl_SI' => 'slovenščina (Slovenija)',
        'sr_YU' => 'sr_YU',
        'sv_SE' => 'svenska (Sverige)',
        'tr_TR' => 'Türkçe (Türkiye)',
        'uk_UA' => 'українська (Україна)',
        'zh_CN' => '中文(简体中文、中国)',
        'zh_HK' => '中文(繁體中文,中華人民共和國香港特別行政區)',
        'zh_TW' => '中文(繁體中文,台灣)'
);

2014年9月19日 星期五

[OSX] 使用 App Store 更新 Xcode 發生錯誤 @ Mac OS X 10.9.5, MBA


主因也有可能是空間太少了 :P
除了準備空間外,可以試看看 App Store Debug 界面:

$ defaults write com.apple.appstore ShowDebugMenu -bool true

接著重新打開 App Store 後,上方最右邊有 Debug 選項 -> Reset Application,大概多做幾次即可。如果很不幸的一直不幸,最終解法就是反安裝後,再下載。

2014年9月18日 星期四

iOS 開發筆記 - 驗證 Apple Push Notification PEM File 以及 Remove PEM Password

半年前曾開發過: 使用 Apple Push Notification service (APNs),最近更新 PEM  後,要驗證一下 PEM 是否正確。

驗證還滿簡單的,單純用 openssl 進行,假設是 dev 模式,對象就是 snadbox:

$ openssl s_client -connect gateway.sandbox.push.apple.com:2195 -cert cert.pem -key key.pem

其中 cert.pem 跟 key.pem 也可以合在一起:

$ cat cert.pem key.pem > out.pem
$ openssl s_client -connect gateway.sandbox.push.apple.com:2195 -cert out.pem


假設產出的 key.pem 有密碼保護,為了圖方便想要去密碼的話,可以用:

$ openssl rsa -in key.pem -out nopass.pem

如此一來就搞定了,若測試的是 Production PEM ,記得改用 gateway.push.apple.com:2195 即可。

$ openssl s_client -connect gateway.push.apple.com:2195 -cert cert.pem -key key.pem

2014年9月12日 星期五

[Python] 使用 Apache Web Server Access.log 把玩 CartoDB 視覺化地圖 @ Ubuntu 12.04



只要資料有 Geolocation,就能夠把玩 CartoDB 了 :P 若可以的話,再加上時間就更完美了。因此,最容易拿到的測資是 Apache web server log,把 access.log 挑點東西出來即可,至於 Geolocation 就用 ip 反查吧!

從 access.log 取出 ip list:

$ grep -v "^localhost\|::1" /var/log/apache2/access.log | awk '{print $1}' | uniq

首先,先到 Maxmind 下載最新的 GeoLite2-City 資訊:

$ wget http://geolite.maxmind.com/download/geoip/database/GeoLite2-City.mmdb.gz
$ gunzip GeoLite2-City.mmdb.gz


安裝 geoip-bin 工具:

$ sudo apt-get install geoip-bin
$ geoiplookup 8.8.8.8
GeoIP Country Edition: US, United States

$ geoiplookup -f GeoLite2-City.mmdb 8.8.8.8
Error Traversing Database for ipnum = 134744072 - Perhaps database is corrupt?
Segmentation fault (core dumped)


囧...只好裝一下新版 maxmind python sdk 寫一段小 code:

$ sudo pip install geoip2

$ vim t.py
mport sys
import geoip2.database
reader = geoip2.database.Reader('GeoLite2-City.mmdb')
try:
        response = reader.city(sys.argv[1])
        print str(response.location.latitude)+","+str(response.location.longitude)
except Exception, e:
        pass
$ python t.py 8.8.8.8
37.386,-122.0838


接著,就乾脆寫 python 來處理 access.log 吧 XDD 用 command line 好像太冗長了。

$ sudo cp /var/log/apache2/access.log /tmp/access.log
$ sudo chmod 644 /tmp/access.log
$ vim log.py
import geoip2.database
reader = geoip2.database.Reader('GeoLite2-City.mmdb')
try:
        log = open('/tmp/access.log','rb').read()
        for rec in log.split('\n'):
                fields = rec.split(' ')
                try:
                        if fields[0] == 'localhost' or fields[0] == '::1' :
                                continue
                        response = reader.city(fields[0])
                        print fields[3][1:]+","+str(response.location.latitude)+","+str(response.location.longitude)
                except Exception, e:
                        pass
except Exception, e:
        pass

$ python log.py > log.csv
$ cat log.csv
...
07/Sep/2014:22:12:43,35.685,139.7514
07/Sep/2014:22:13:53,39.4899,-74.4773
...


對於時間格式不用擔心,直接丟進 cartodb.com 請他幫你處理!




匯入後,預設都是 string,可以把 field1 設成 date type,field2 跟 field3 都設成 number type,弄完順便 rename 一下,接著再點選 geo 欄位,可以採用 field2 跟 field3 來生成,如此就完成 CartoDB  table 製作。








最後再去視覺化那邊,挑一下以 date 為基準的時間變化,就可以有不錯的視覺圖表。

2014年9月11日 星期四

Android 開發筆記 - 透過 gsutil 取得 Google Play App Customer Reviews

依照官方文件簡介,對於 Android app 的評論,系統都會儲存在 Google Cloud Storage 裡頭,需要透過 gsutil 取出來使用,在此透過 pip 安裝 gsutil:

$ sudo pip install gsutil

使用前,需要先到 Google Play Developer Console -> Your android app -> 評分與評論 -> 底下最下方找到 ID,如 pubsite_prod_rev_0123456789。

接著,先設定 gsutil 存取權限:

$ gsutil config
This command will create a boto config file at /home/username/.boto
containing your credentials, based on your responses to the following
questions.
Please navigate your browser to the following URL:
https://accounts.google.com/o/oauth2/auth?scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdevstorage.full_control&redirect_uri=urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob&response_type=code&client_id=############.apps.googleusercontent.com&access_type=offline
In your browser you should see a page that requests you to authorize access to Google Cloud Platform APIs and Services on your behalf. After you approve, an authorization code will be displayed.

Enter the authorization code:


並透過瀏覽器得到 authorization code,輸入完後,會在詢問 Project ID,這時請記得填寫類似這串 pubsite_prod_rev_0123456789 的 ID。

之後,就可以透過 gsutil cp -r gs://pubsite_prod_rev_0123456789/reviews/reviews* 取得所有評論。

$ gsutil cp -r gs://pubsite_prod_rev_0123456789/reviews/reviews_* android_review/

這些 reviews 都是 CSV 格式,且第一列有顯示欄位資訊:

Package Name,App Version,Reviewer Language,Reviewer Hardware Model,Review Submit Date and Time,Review Submit Millis Since Epoch,Review Last Update Date and Time,Review Last Update Millis Since Epoch,Star Rating,Review Title,Review Text,Developer Reply Date and Time,Developer Reply Millis Since Epoch,Developer Reply Text,Review Link

看來只要寫隻簡易的 tool 就可以處理完畢囉:

$ file reviews_*.csv
reviews_*.csv: Little-endian UTF-16 Unicode text, with very long lines


因此,寫 php script 的話,可以先用 iconv 轉 UTF-8 後,再搭配 str_getcsv 處理:

<?php
$raw = file_get_contents(...);
$result = iconv($in_charset = 'UTF-16LE' , $out_charset = 'UTF-8' , $raw);
if( false !== $result )
$raw = $result;

$raw_lines = explode("\n", $raw);
array_shift($raw_lines); // title
foreach( $raw_lines as $line ) {
$fields = str_getcsv($line);
print_r($fields);
}

iOS 開發筆記 - 透過 iTunesConnect RSS 處理 App Customer Reviews 資料

以 Facebook 而言,可以輕易得知 App ID 為 284882215,而 iTunesConnect 有提供 RSS 方式查詢 Customer Reviews:

https://itunes.apple.com/us/rss/customerreviews/id=284882215/sortBy=mostRecent/xml

其中,上述有三個主要可變動的參數:country(us), app id (284882215), format(xml)

最近比較愛 json 的:

https://itunes.apple.com/us/rss/customerreviews/id=284882215/sortBy=mostRecent/json

想要台灣區評價:

https://itunes.apple.com/tw/rss/customerreviews/id=284882215/sortBy=mostRecent/json

問題應該就是 country 到底有哪些,可以用既定資料每個都抓一遍啦。

對於格式有興趣的可以用得到可讀性較佳的資料格式:

$ curl https://itunes.apple.com/tw/rss/customerreviews/id=284882215/sortBy=mostRecent/json | python -mjson.tool

若仔細看的話,還可以看到 User ID 、追蹤到 User 對其他款 app 評價等,此外,也有 first page 跟 last page 存取方式:

https://itunes.apple.com/tw/rss/customerreviews/page=1/id=284882215/sortby=mostrecent/json
https://itunes.apple.com/tw/rss/customerreviews/page=10/id=284882215/sortby=mostrecent/json

最大頁數只有到 10 而已(CustomerReviews RSS page depth is limited to 10)

至於要用 json 還是 xml 好?目前的心得是... json 會缺少 user comment updated 資訊,而 xml 卻也會碰到 format error 的情況 Orz 只能...看著辦了 XD


以 PHP 處理為例:

$ cat test.php
<?php
$app_id = '284882215';
$country_list = array( 'tw', 'us');
$format = 'xml';
$page_list = array(1,2,3,4,5,6,7,8,9,10);
foreach( $country_list as $country ) {
        foreach( $page_list as $page ) {
                $url = "https://itunes.apple.com/$country/rss/customerreviews/id=$app_id/page=$page/$format";
                if ($format == 'json') {
                        $raw = json_decode(@file_get_contents($url), true);
                        //print_r($raw);
                        for ($i=1, $cnt=count($raw['feed']['entry']) ; $i<$cnt ; ++$i) {
                                print_r(array(
                                        'user_id' => $raw['feed']['entry'][$i]['id']['label'],
                                        'user_name' => $raw['feed']['entry'][$i]['author']['name']['label'],
                                        'user_uri' => $raw['feed']['entry'][$i]['author']['uri']['label'],
                                        'title' => $raw['feed']['entry'][$i]['title']['label'],
                                        'content' => $raw['feed']['entry'][$i]['content']['label'],
                                        'version' => $raw['feed']['entry'][$i]['im:version']['label'],
                                        'rating' => $raw['feed']['entry'][$i]['im:rating']['label'],
                                ));
                        }
                } else {
                        $raw = simplexml_load_string(@file_get_contents($url));
                        //print_r($raw);
                        for($i=1, $cnt = count($raw->entry); $i<$cnt ; ++$i) {
                                //print_r($raw->entry[$i]);
                                $imAttrs = $raw->entry[$i]->children('im', true);
                                //print_r($imAttrs);
                                print_r(array(
                                        'date' => (string)$raw->entry[$i]->updated,
                                        'user_id' => (string)$raw->entry[$i]->id,
                                        'user_name' => (string)$raw->entry[$i]->author->name,
                                        'user_uri' => (string)$raw->entry[$i]->author->uri,
                                        'title' => (string)$raw->entry[$i]->title,
                                        'content' => (string)$raw->entry[$i]->content[0],
                                        'version' => (string)$imAttrs->version,
                                        'rate' => (string)$imAttrs->rating,
                                ));
                        }
                }
                exit;
        }
}

$ php test.php
Array
(
    [date] => 2014-09-10T09:10:00-07:00
    [user_id] => ###########
    [user_name] => ###########
    [user_uri] => https://itunes.apple.com/tw/reviews/id###########
    [title] => 有Bug請改善
    [content] => 點朋友的動態跑不出來!!
    [version] => 14.0
    [rating] => 1
)
Array
(
    [date] => 2014-09-10T09:03:00-07:00
    [user_id] => ###########
    [user_name] => ###########
    [user_uri] => https://itunes.apple.com/tw/reviews/id###########
    [title] => 一直自動關掉
    [content] => 用一用會一直自動跳掉,很頻繁,很煩。
    [version] => 14.0
    [rating] => 1

)

2014年9月10日 星期三

[SQL] SELECT IN SELECT 以及 Pagination 的使用 @ MySQL 5.6

使用 SQL 語法時,有時會需要從另一張 Table 取出清單,接著對清單內的資料做為基準再進行一次資料的擷取,直觀的想法大概是 SELECT something FROM Table1 WHERE id IN (SELECT id FROM Table2 WHERE ... )。

可惜上述語法是不行的 XD 要改成 JOIN 的做法:

SELECT something FROM Table1, (SELECT id FROM Table2) AS list WHERE Table1.id = list.id;

接著,偶爾會需要 pagination 的需求,加個 LIMIT 的用法,這時候又會想要回報全部有幾筆資料(對於一些搜尋引擎的設計,有些是採用預估的方式),以便前端可以估算有幾筆資料。

最簡單的解法是再用一個 SQL Query 去問 Table2 的 id 資料,但想要更快一點,就來試試 MySQL User-Defined Variables 吧!

SELECT something, @n AS total FROM Table1, (SELECT id, CASE WHEN @n > 0 THEN @n := @n + 1 ELSE @n := 1 END AS n FROM Table2, (SELECT @n := 0) AS init) AS list WHERE Table1.id = list.id;

如此一來,結果都會有個 total 筆數跟著,雖然仍不夠好,但也不錯啦 XD  而搭配 LIMIT OFFSET,COUNT 時,total 的資訊是來自掃 Table2 的資料,所以也能正常顯示:

SELECT something, @n AS total FROM Table1, (SELECT id, CASE WHEN @n > 0 THEN @n := @n + 1 ELSE @n := 1 END AS n FROM Table2, (SELECT @n := 0) AS init) AS list WHERE Table1.id = list.id LIMIT 0,10;

[C++] 使用 PCRE、RE2 進行 Match all (如同 PHP preg_match_all 效果) @ Ubuntu 14.04

對 PHP 來說:

$ vim t.php
<?php
$data = "..<a href='...'>...</a>.."; //file_get_contents(...);
if (preg_match_all("#<a[^h]*href=['\"]{0,1}([^\"']+)[\"']{0,1}[^>]*>(.*?)</a>#", $data, $matches) )
        print_r($matches);
$ php t.php
Array
(
    [0] => Array
        (
            [0] => <a href='...'>...</a>
        )

    [1] => Array
        (
            [0] => ...
        )

    [2] => Array
        (
            [0] => ...
        )

)


對 PCRE 來說:

$ vim pcre_test.cpp
#include <pcre.h>
#include <iostream>

int main() {

const char *error;
int erroroffset;
pcre *preg_pattern_a_tag = pcre_compile("<a[^h]*href=['\"]{0,1}([^\"']+)[\"']{0,1}[^>]*>(.*?)</a>", PCRE_MULTILINE, &error,  &erroroffset, NULL);

if (!preg_pattern_a_tag) {
std::cout << "ERROR\n";
return -1;
}

std::string raw = "..<a href='...'>...</a>..";

unsigned int offset = 0;
unsigned int len = raw.size();
int matchInfo[3*2] = {0};
int rc = 0;

while (offset < len && (rc = pcre_exec(preg_pattern_a_tag, 0, raw.c_str(), len, offset, 0,  matchInfo, sizeof(matchInfo))) >= 0) {
for (int n=0; n<rc ; ++n) {
int data_length = matchInfo[2*n+1] - matchInfo[2*n];
std::cout << "Found:[" << raw.substr(matchInfo[2*n], data_length) << "]\n";
}
offset = matchInfo[1];
}
return 0;
}
$ g++ -std=c++11 pcre_test.cpp -lpcre
$ ./a.out
Found:[<a href='...'>...</a>]
Found:[...]
Found:[...]


對 RE2 來說:

$ vim re2_test.cpp
#include <re2/re2.h>
#include <iostream>

int main() {

//RE2 preg_pattern_a_tag("<a[^h]*href=['\"]{0,1}([^\"']+)[\"']{0,1}[^>]*>(.*?)</a>", RE2::Latin1);
RE2 preg_pattern_a_tag("<a[^h]*href=['\"]{0,1}([^\"']+)[\"']{0,1}[^>]*>(.*?)</a>");

std::string raw = "..<a href='...'>...</a>..";

re2::StringPiece result_a_href, result_a_body;

while(RE2::PartialMatch(raw, preg_pattern_a_tag, &result_a_href, &result_a_body)) {
std::cout << "result_a_href:[" << result_a_href << "]\n";
std::cout << "result_a_body:[" << result_a_body << "]\n";
raw = result_a_body.data();
}
return 0;
}
$ g++ -std=c++11 re2_test.cpp /path/libre2.a -lpthread
$ ./a.out
result_a_href:[...]
result_a_body:[...]

2014年9月3日 星期三

AWS 筆記 - 使用 Amazon Route53 設定 DNS SPF Record

如果說 MX 是用來提供寄件者得知 mail server 在哪邊,那 SPF 則是提供 mail receiver 驗證寄件者使用的 mail server 是不是接近已知、合法的 server。因此,設定 DNS SPF Record 是會降低信件誤判成垃圾郵件的機率。

採用 Amazon Route53 時,設定 TXT Record (有 SPF Record,但這次沒試)

"v=spf1 a mx a:YourServerHostname  ~all"

最後面的 ~all 代表上述未定義的,可能是錯的。若要強制歸類在錯誤,就用 -all 即可。

驗證方式(此例以 cs.nctu.edu.tw 為例):

$ nslookup -q=txt cs.nctu.edu.tw
...
cs.nctu.edu.tw  text = "v=spf1 a mx a:farewell.cs.nctu.edu.tw a:csmailer.cs.nctu.edu.tw a:tcsmailer.cs.nctu.edu.tw a:csmailgate.cs.nctu.edu.tw a:csmail1.cs.nctu.edu.tw a:csmail2.cs.nctu.edu.tw a:csws1.cs.nctu.edu.tw a:csws2.cs.nctu.edu.tw ~all"
...


可以看到 "v=spf1 ... " 資訊,代表 DNS TXT Record 設定無誤,記得這是有 dns cache 機制,不見得馬上更改或是馬上可以查詢到。

接著,可以用 Gmail 來測試,從自家 mail server 寄信到 Gmail,若沒有設定 DNS SPF Record ,會顯示:

Received-SPF: none (google.com: YourSender@YourMailServer does not designate permitted sender hosts) client-ip=YourMailServerIP;

若有設定 DNS SPF Record 但不在名單內,此例為 ~all 用法:

Received-SPF: softfail (google.com: domain of transitioning YourSender@YourMailServer does not designate YourMailServerIP as permitted sender) client-ip=YourMailServerIP;

若設定正確即在 DNS SPF Record 定義內:

Received-SPF: pass (google.com: domain of YourSender@YourMailServer designates YourMailServerIP as permitted sender) client-ip=YourMailServerIP;

要留意,如果租的機器是在一些常見的 VPS 時,對外走的可能是 IPv6 ,此時記得 DNS Reocrd 中,也要用 AAAA Record (IPv6) 定義。

2014年9月2日 星期二

[Linux] 透過 alias 與 virtual 機制建立 no-reply @ hostname 帳號 @ Postfix 2.9.6, Ubuntu 12.04

新增一筆 alias 用來導向 /dev/null

$ grep alias_maps /etc/postfix/main.cf
alias_maps = hash:/etc/aliase
$ sudo vim /etc/aliases
...
devnull: /dev/null
...


採用 virtual account 新增 no-reply 帳號:

$ grep virtual_alias_maps /etc/postfix/main.cf
$ sudo vim /etc/postfix/main.cf
virtual_alias_maps = hash:/etc/postfix/virtual

$ sudo vim /etc/postfix/virtual
no-reply@hostname devnull

$ sudo newaliases
$ sudo postmap /etc/postfix/virtual
$ sudo postfix reload
postfix/postfix-script: refreshing the Postfix mail system


至於測試方式,就直接連他寄信看看:

$ telnet localhost 25
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
220 hostname ESMTP Postfix (Ubuntu)
HELO localhost
250 hostname
mail from:<root@localhost>
250 2.1.0 Ok
rcpt to:<no-reply@hostname>
250 2.1.0 Ok
quit
221 2.0.0 Bye
Connection closed by foreign host.


如果出現 451 4.3.0 <no-reply@hostname>: Temporary lookup failure ,請記得翻一下 /var/log/mail.err ,若是單純 error: open database /etc/postfix/virtual.db: No such file or directory 問題,記得跑一下 sudo postmap /etc/postfix/virtual 即可。