作者:cschen33.tw@yahoo.com.tw
發展中.....
參考資料:
hfaxd手冊,hylafax-client手冊,w2hfax的 原始程式(hfaxlib.py)(w2hfax的程式已經停止發展了)

前言:
要寫一個,windows版的傳真軟體,其主要操作流程有兩種:
1.列印傳真
開啟文件->列印->印表機(產生傳真格式文件)- >自己的程式(傳送到傳真伺服器)
2.直接啟動程式
選取傳真的文件(ps)->傳送到傳真伺服器
經過簡化,我們可以分為兩個部份
1.產生列印檔
2.與傳真伺服器之間的溝通
註:因應不同的環境所要考慮的client程式有所不同
1.同一伺服器上執行:
  若是同一個伺服器下,我們不需要直接執行hylafax的通訊協定,在程式中直接引用sendfax命令,
  #sendfax的傳送檔案格式可以是,內定是(ps,tiff),但是它會將文字檔及PDF,SGI等檔案轉為Ps或Tiff
  #因此如網頁上的傳真就可以直接使用sendfax命令
2.在不同伺服器上執行:
  不同的伺服器必須透過網路的連線,因此我們必需使用hylafax的通訊協定


首先我們要先了解基本通訊協定的溝通原理:

根據hfaxd手冊得hylafax的通訊協定是類似ftp的協定(我不考慮舊的snpp協定):
實例:
取自hylafax-client手冊
下面的命令顯示如何使用FTP client程式直接和HylaFAX 伺服器通訊:

hyla% ftp localhost hylafax
Connected to localhost.
220 hyla.chez.sgi.com server (HylaFAX (tm) Version 4.0beta005) ready.
Name (localhost:sam):
230 User sam logged in.
Remote system type is UNIX.
Using binary mode to transfer files.
ftp> dir sendq
200 PORT command successful.
150 Opening new data connection for "sendq".
208 126 S sam 5268781 0:3 1:12 16:54 No local dialtone
226 Transfer complete.
ftp> quote jkill 208
200 Job 208 killed.
ftp> dir doneq
200 PORT command successful.
150 Opening new data connection for "doneq".
208 126 D sam 5268781 0:3 1:12 No local dialtone
226 Transfer complete.
ftp> quote jdele 208
200 Job 208 deleted; current job: (default).
ftp> dir docq
200 PORT command successful.
150 Opening new data connection for "docq".
-rw---- 1 sam 11093 Jan 21 16:48 doc9.ps
226 Transfer complete.
ftp> dele docq/doc9.ps
250 DELE command successful.
ftp> dir recvq
200 PORT command successful.
150 Opening new data connection for "recvq".
-rw-r-- 4 fax 1 510 5268781 30Sep95 faxAAAa006uh
-rw-r-- 9 fax +14159657824 11Nov95 faxAAAa006nC
-rw---- 25 fax +14159657824 Fri08PM fax00016.tif
226 Transfer complete.
ftp> quit
221 Goodbye.

在hfaxd所提及可以使用ftp來通訊,但是你的ftp client程式必須能使用quote的命令
quote是用來執行任意的命令,經測試在win98的ftp是可以使用的


通訊協定分析:
和hylafax溝通有兩種方式,telnet或ftp
一、telnet

  1. 通訊命令注意事項
  2. hylafax和一般telnet的程式不一樣的地方,連線成功後,碰到目錄查詢 (LIST),檔案的傳送及一些特殊命令時,是透過被動模式的連線來傳送(另開一個連線,至於連接埠,由第一個連線(命令PASV)隨機指定),因此你至 少要存在兩個連線才可以執行命令:
    1.接收伺服器的資料:伺服器傳送完成後會立即關閉被動模式的連線
    2.傳送資料到伺服器:當你關閉被模式代表,資料已經送完
    總而言之,當有上述何命令時要先執行PASV以取得連接埠,然後新增一個連線,來傳送及接收資料

    網路的通訊是一來一往的,也就是傳送命令->接收結果->傳送->接收............
    寫程式也是一進一出,不管是結果是不是你需要的,只要送出命令,都要將結果接收回來,
    不可以傳送->傳送->接收->接收           (我的程式會當機)
  3. passive模式的連接埠的計算公式
  4.   我們要傳檔到伺服器時要使用被動模式
      命令PASV
      會傳回(192,168,0,50,15,62)
    前四位數為IP=192.168.0.50後兩個為連接埠分別為P1及P2
    P1*256+P2
  5. 傳真步驟
  6. 1.傳送傳真:
    連線主機(session1)
    使用者名稱
    送使用者密碼(如果需要的話)
    進入passive模式
    取得及計算被動模式連接埠
    新增連線指定連接埠為取得之被動模式連接埠(session2)

    傳送檔案
    關閉被動模式連線
    telnet  host 4559
    user username
    pass password
    pasv
    telnet host 被動模式的port
    stot    #回到先前的連線輸入

    job default
    jnew
    jparm ..........
    jsubm jobid
    連線
    使用者名稱
    使用者密碼
    進入被動模式
    建立新的連線
    儲存檔案(檔案必須是ps)
    將資料寫入被動模式連線後關閉(檔案就傳送到伺服器)

    新增工作(要記住jobid)
    設定工作參數如要傳送的檔案所在,傳真號碼......等
    交付傳真的工作

         ●檔案的格式必須是PS檔,它不像sendfax可以接收不同的檔案格,只能傳送PS檔(whfc也是ps)
            副檔名可以在執行命令stot時看的到都是.ps
         ○所有的輸入命令後面都不可以加空白,否則會有錯誤訊息
    jparm是用來指定傳真工作的所有參數,其必要的命令(否則會產生錯誤)
       
    DOCUMENT
    指定你要傳送的傳真檔案
    DIALSTRING
    就是傳真號碼
    LASTTIME
    最後傳送時間,指定系統這個傳真檔傳真後多久要刪除要刪除的時間
    jparm dialstring 23218811
    jparm lasttime 000300  ,傳真失敗中斷時間,好像是秒
    這個參數最好也給
    NOTIFYADDR username@your_company_domain
    它會在傳真傳送成功後寄一封通知信給該使用者
    NOTIFY [NONE|DONE|EQUEUE|DONE+REQUEUE]
    上述是通知的設定
    SCHEDPRI 數字
    設定排程權限的大小(愈小愈高)0-255,內定127
  7. 查詢傳真步驟
  8.   使用命令
      1.LIST sendq:準備要傳送的
      2.LIST doneq:剛傳完(不一定是成功,也包含中斷)
      3.LIST recvq:收到的傳真

  9. 實作
  10. 目前我們知了運作的原理,現在要以python的來寫一個連線程式
    已知hylafax的資訊:
    主機ip或名稱:例,localhost
    連接埠:內定4559
    使用的模組socket
    from socket import *
    s1=socket(AF_INET,SOCK_STREAM)


    s1.connect(("localhost",4559))
    s1_f=s1.makefile("rw")
    line=s1_f.readline()
    s1.send("User csc \n")
    line=s1_f.readline()
    #為什麼要如此作,如果沒有這麼作socket(AF_INET,SOCK_STREAM)
    要改寫為socket.socket(socket.AF_INET,socket.SOCK_STREAM)
    建立s1為socket的資料結構
    建立連線
    將socket回傳的資料以檔案方式來讀取
    注意讀取是以s1_f.readline()
    寫入是以s1.send("資料\n")
    而寫入資料一定要換行才代表資料已經傳出去,否則讀取的資料無法得到回應
二、ftp
  1. 使用ftp協定來和傳送及收取傳真(非常簡單,才知道我浪費多少時間在上述的解析)
  2. 如果你是使用whfc1.2之前的軟體你可能會發現,因為使用被動模式而無法在設有防火牆的伺服器中透過網際網路來傳真,但是1.1之後則是使用ftp的 方法,因此可以不使用被動模式,所以可以透過網際網路來傳真,因此傳真的問題可以說是迎刃而解,也就是你可以使用一般的ftp指令來傳檔,查詢,若碰到有 一些專用指令則使用quote來執行
    傳送傳真
    ftp  host 4559
    username
    password
    bin

    cd /tmp
    put  檔名
    quote job default
    quote jnew
    quote jparm ..........
    quote jsubm jobid
    連線
    使用者名稱
    使用者密碼
    要先確定你資料的格式如果是tif,pdf使用bin
    否則使用ascii(ps,txt)
    建立新的連線
    儲存檔案(檔案必須是ps)

    新增工作(要記住jobid)
    設定工作參數如要傳送的檔案所在,傳真號碼......等

    查詢傳真
    ftp  host 4559
    username
    password
    dir  recvq
    lcd c:\windows\temp
    bin
    get 檔名
    連線
    使用者名稱
    使用者密碼


    夠簡單吧
    以下是查到的收到fax的內容:
    同理我們可以查到sendq準備傳送的傳真,doneq剛傳送完畢的傳真


  3. python的ftplib
  4. FTP物件中用的到的方法

    connect(host[, port]):連線主機

    login([user[, passwd[, acct]]]): 登入

    abort()
    取消正在傳送的檔案.
    sendcmd(command)
    傳送一個簡單的命令到伺服器,並傳回回應字串.
    retrbinary(command, callback[, maxblocksize[, rest]])
    command 必須是RETR ....,
    取回檔案使用二位元檔模式.相當於bin及get 檔案,callback,一般是一個檔案物件write函式
    retrlines(command[, callback])
    使用ASCII模式來取回目錄或是檔案.command 必須是 "RETR".在 每取得一行時callback 函式會被呼叫.內定值是會顯示在sys.stdout.
    storbinary(command, file[, blocksize])
    儲存一個檔案在二位元傳送模式. command應該使用適當"STOR" 命令: "STOR filename".blocksize是 在2.1版被新增,其內定值是8192.
    storlines(command, file)
    儲存一個檔案在ASCII的傳輸模式. command 應該使用適當"STOR" 命令 (參照storbinary()).
    nlst(argument[, ...])
    以list傳回檔名
    dir(argument[, ...])
    以字串傳回目錄內容.
    delete(filename) 刪除檔案.
    cwd(pathname)
    設定在伺服器上來源目錄.
    mkd(pathname)
    建立一個新目錄.
    pwd()
    傳回路徑.
    rmd(dirname)
    刪除目錄.
    size(filename)
    查詢檔案的大小
    quit()
    較有禮貌的離開
    close()
  5. 實作
  6. 取得檔案
    >>>import ftplib
    >>>ftp=ftplib.FTP()
    >>>ftp.connect(host="192.168.0.50",port=4559)
    '220 www.ocean-pioneer.com server (HylaFAX (tm) Version 4.1.5) ready.'
    >>> ftp.login(user="csc")
    '230 User csc logged in.'
    >>> ftp.cwd('recvq')
    '250 CWD command successful.'
    >>> ftp.dir()
    -rw-rw- 1 14 886223512188 Tue06AM fax05248.tif
    -rw-rw- 2 14 886 2 23579359 Tue07AM fax05249.tif
    -rw-rw- 1 14 Tue08AM fax05250.tif
    -rw-rw- 886 2 23579359 Tue08AM fax05251.tif
    -rw-rw- 1 14 886 2 23579359 Tue09AM fax05253.tif
    >>>ftp.retrbinary('retr fax05250.tif',open('c:\\temp\\test.ps','wb').write)
    '226 Transfer complete.'
    >>>
    傳送檔案
    >>>import ftplib
    >>>ftp=ftplib.FTP()
    >>>ftp.connect(host="192.168.0.50",port=4559)
    '220 www.ocean-pioneer.com server (HylaFAX (tm) Version 4.1.5) ready.'
    >>>ftp.login(user="csc")
    '230 User csc logged in.'
    >>>ftp.cwd('tmp')
    '250 CWD command successful.'
    >>>ftp.storlines('stor b.ps',open("c:\\test.ps"))
    '226 Transfer complete.'
    >>>
  7. 回應訊息(成功的)
  8. connect
    220 www.ocean-pioneer.com server (HylaFAX (tm) Version 4.1.5) ready.'
    login
    230 User csc logged in.
    cwd
    250 CWD command successful.
    dir
    有內容則顯示,沒有則空白
    sendcmd('job default')
    200 Current job: (default).
    sendcmd('jnew')
    200 New job created: jobid: 3222 groupid: 3222.
    必須在根目錄下才可以下
    sendcmd('jparm ...')
    dialstring ->213
    lasttime   ->213
    document   ->200
    notifyaddr ->213
    sendcmd('jsubm jobid')
    200
    sendcmd('jkill jobid')
    200 Job 208  deleted ; current job: (default).
    sendcmd('filefmt')

    sendcmd('jobfmt')

    retrbinary()
    226 Transfer complete.
    storlines()
    226 Transfer complete.
    jnew
    如何取回工作id
    例:
    retvalue=ftp.sendcmd('jnew')
    jobid=retvalue[retvalue.find('jobid:')+6:retvalue.find('groupid:')]
    ========
    如何查詢目錄內容,刪除(sendq),檢視(recvq)
    刪除是以jobid為主
    檢視則是以檔案名稱為主,因為我們要取回來檢視
    使用.nlst('目錄')

根據上述的分析,我自己寫了一個類別:faxlib
說明:這個一類別是使用ftp的方式來和hylafax溝通
     python的類別使用要點:將hylafxlib.pyc放在適當的地方
用法:import hylafxlib
     faxtest=faxlib.faxlib()
     faxtest.connect('host',4559,'User','passwd')
     如此連線完成
     faxtest.send_fax('23512188','c:\\windows\\desktop\\a.ps','abc@ocean-pioneer.com')
     上述完成交付工作到傳真伺服器
類別fxlib.faxlib的API
唯一的問題是:目前我無法使用刪除上傳到/tmp的暫存檔
我使用刪除命令無效delete('file'),sendcmd('dele file')
都會說權限不夠(如果是使用telnet的方式(被動模式)它會使用一個命令叫stot 它會到docq中查詢seqf的內容然後為暫存檔的名稱,docxxxx.ps,xxxx為查到的數字,然後加1回存,等待連線結束時則檔案會自動刪除)
解決方法:被動連線傳檔時使用socket的方法,而主動連線還是使用FTP的函式使用 hylafax的命令是STOT,我寫的時碰到另一個問題就是無法在執行連線之後正確的取得資料,最後得到結論是連線的模式是送出一個命令之後必須讀回命 令,而在傳檔結束時關閉socket連線回傳回一個結果到使用ftp的連線,而ftp模組並沒有這個命令,因此我去查了ftp物件找到下面的命令
self.ftp_object.getline()
結果成功
API
說明
connect(host,port,user,passwd)
登入,成功傳回1,回應字串(伺服器的狀態),失敗-1,回應字串(伺 服器的狀態)
login()
登出,成功傳回1,失敗-1
quit()
登出,成功傳回1,失敗-1
get_jobid()
經由jnew回傳狀態,取得工作ID
put_file(target_file,source_file,mode)
上傳檔案,模式分a/b(文字/二位元)
get_file(target_file,source_file))
下載檔案
send_fax(dialstring,document,notifyaddr,jobfmt)
傳送傳真(電話號碼,client上欲傳送的檔案,e-mail通知, 查詢的工作模式)
query_recvq()
查詢recvq目錄中的傳真檔,傳回檔案物件
回傳兩個變數,1-狀態,2-檔案物件/錯誤訊息
query_sendq()
查詢sendq目錄中的傳真檔,傳回檔案物件
同上
query_doneq()
查詢doneq目錄中的傳真檔,傳回檔案物件
同上
get_recv(filename)
下傳recvq中的傳真檔案(*.tif)
回傳兩個變數,1-狀態,2-儲存的檔案/錯誤訊息
abort_fax(jobid)
取消傳真(只有待傳的才可以)
query_recvq_list()
查詢recvq目錄中的傳真檔,傳回檔案的list,為了取得檔名容易
  

命令列下的傳真client端程式 (在命令視窗下傳送傳真,傳真檔僅接受ps檔案)
由上面的程式我加以改寫成一個命令列的傳送程式,使用py2exe程式編譯成為執行檔
用法如下:
usage='''usage:
hfax_cmd -H host -P port -u user -p passwd -m notify-email -f faxfile -n fax number

arguments:
-H host        -need
-P port
-u username    -need
-p password
-m notify email address
-f fax file    -need
-n fax number  -need
-v or -V get version information

例如 hfax_cmd -H 192.168.3.123 -u andy -m andy@mycompany.com.tw -f c:\\a.ps -n 25810101
1.原始程式hfax_cmd.py
2.py2exe的編譯程式hfax_cmd_setup.py
3.編譯完成的程式hfax_cmd.zip


程式的設定檔
1.傳真環境設定
2.傳真電話簿的設定

傳真環境檔的設定有兩種方法:
    
文字檔
對使用者來講設定會比較方便
登錄檔
不知有何優點,一般軟體所在為 HKEY_LOCAL_MACHINE.Software.下寫入你程式的名稱,
其中的鍵值可以自定(數字,文字)
必要資訊如下,
host
port
user
passwd
e-mail



以下尚未完成......
如何讓自己的程式可以被其它程式呼叫(com,active X,ole)

class test():
    _public_methods_ = ["函式1","函式2","函式..."]
    _reg_progid_ = "程式名稱"
    _reg_clsid_ = "{243E9E30-0702-11D4-8088-005004EFAAEE}"
    以下定義函式
    def 函式1:
    ...........
    def 函式2:
    ...........
    def 函式...


if __name__=="__main__":
    print "Registering COM Server..."
    import win32com.server.register
    win32com.server.register.UseCommandLine(test)